From bea33e1c955d8eb18a520913a485d8ae180f0e04 Mon Sep 17 00:00:00 2001 From: Commander1024 Date: Wed, 16 Sep 2026 14:04:34 +0200 Subject: [PATCH] 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. --- cmake/security_overrides.cmake | 29 + docs/agent/code-map.md | 11 +- docs/agent/current-state.md | 9 + docs/dependency_licenses.md | 30 +- docs/roadmap.md | 2 +- docs/security_hardening.md | 26 +- docs/wolf_security_review.md | 24 +- tests/sdk_security_overrides/run.py | 66 +- tests/ssh_protocol_policy/run.py | 36 +- tests/ssh_protocol_policy/vendor.c | 20 +- tests/wolf_crypto_policy/run.py | 2 +- tests/wolfssh_order_contract/README.md | 182 ++ tests/wolfssh_order_contract/contract.c | 288 +++ tests/wolfssh_order_contract/interop.c | 175 ++ tests/wolfssh_order_contract/paired.c | 115 ++ tests/wolfssh_order_contract/pio_adapter.py | 105 ++ tests/wolfssh_order_contract/run.py | 314 ++++ tests/wolfssh_order_contract/user_settings.h | 43 + tests/wolfssh_parser_contract/run.py | 7 +- tools/security_overrides.py | 50 +- tools/wolfssh_order/793.patch | 882 +++++++++ tools/wolfssh_order/819.patch | 46 + tools/wolfssh_order/840.patch | 147 ++ tools/wolfssh_order/855.patch | 1709 ++++++++++++++++++ tools/wolfssh_order/921.patch | 32 + tools/wolfssh_order/README.md | 155 ++ tools/wolfssh_order/delta.json | 161 ++ tools/wolfssh_order/provenance.json | 51 + 28 files changed, 4653 insertions(+), 64 deletions(-) create mode 100644 tests/wolfssh_order_contract/README.md create mode 100644 tests/wolfssh_order_contract/contract.c create mode 100644 tests/wolfssh_order_contract/interop.c create mode 100644 tests/wolfssh_order_contract/paired.c create mode 100644 tests/wolfssh_order_contract/pio_adapter.py create mode 100644 tests/wolfssh_order_contract/run.py create mode 100644 tests/wolfssh_order_contract/user_settings.h create mode 100644 tools/wolfssh_order/793.patch create mode 100644 tools/wolfssh_order/819.patch create mode 100644 tools/wolfssh_order/840.patch create mode 100644 tools/wolfssh_order/855.patch create mode 100644 tools/wolfssh_order/921.patch create mode 100644 tools/wolfssh_order/README.md create mode 100644 tools/wolfssh_order/delta.json create mode 100644 tools/wolfssh_order/provenance.json diff --git a/cmake/security_overrides.cmake b/cmake/security_overrides.cmake index 26c4e7e..a66f67e 100644 --- a/cmake/security_overrides.cmake +++ b/cmake/security_overrides.cmake @@ -28,6 +28,11 @@ endif() include("${CMAKE_BINARY_DIR}/security_overrides/manifest.cmake") set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${_sak_security_script}" "${SAK_SECURITY_VERSION_HEADER}") +if(SAK_SECURITY_HEADER_IDS) + get_filename_component(_sak_security_tools "${_sak_security_script}" DIRECTORY) + set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS + "${_sak_security_tools}/wolfssh_order/delta.json") +endif() # Public extension point: the Python Entry registry supplies the mapping. This # function is backend-agnostic; a later pinned project/vendor source uses it too. @@ -112,6 +117,30 @@ function(sak_security_replace_source component original generated nested_target) message(STATUS "Security override: ${component}: ${original} -> ${generated}") endfunction() +# HandshakeInfo and WOLFSSH are ABI-sensitive. Force the same generated header +# into the library AND every transitive consumer, even if a consumer adds a +# vendor include directory ahead of the overlay. The original include guard +# then prevents a second, stale definition. This also creates compiler deps. +foreach(_sak_security_id IN LISTS SAK_SECURITY_HEADER_IDS) + if(NOT _sak_security_id STREQUAL "wolfssh_internal_header" OR + NOT SAK_SECURITY_${_sak_security_id}_COMPONENT STREQUAL "wolfssl__wolfssh") + message(FATAL_ERROR "Security override: unaudited header overlay") + endif() + idf_component_get_property(_sak_security_wolfssh wolfssl__wolfssh COMPONENT_LIB) + if(NOT TARGET "${_sak_security_wolfssh}") + message(FATAL_ERROR "Security override: missing wolfSSH overlay target") + endif() + target_include_directories("${_sak_security_wolfssh}" BEFORE PUBLIC + "${SAK_SECURITY_WOLFSSH_INCLUDE}") + # PlatformIO's ESP-IDF adapter sorts app flags and deduplicates component + # flags. Keep option+operand atomic, as with wolf_crypto_policy.h. + target_compile_options("${_sak_security_wolfssh}" PUBLIC + "-include${SAK_SECURITY_${_sak_security_id}_GENERATED}") + set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS + "${SAK_SECURITY_${_sak_security_id}_ORIGINAL}" + "${SAK_SECURITY_${_sak_security_id}_GENERATED}") +endforeach() + foreach(_sak_security_id IN LISTS SAK_SECURITY_OVERRIDE_IDS) sak_security_replace_source( "${SAK_SECURITY_${_sak_security_id}_COMPONENT}" diff --git a/docs/agent/code-map.md b/docs/agent/code-map.md index 8149a9c..eb3813c 100644 --- a/docs/agent/code-map.md +++ b/docs/agent/code-map.md @@ -20,11 +20,12 @@ This is a semantic map, not a complete file inventory. Start here, then read the ## Source-pinned dependency corrections (Phases 9C–9D) - Files: root `CMakeLists.txt` (after `project()`), `cmake/security_overrides.cmake`, `tools/security_overrides.py`; tests: `tests/sdk_security_overrides/run.py --build-dir .pio/build/esp32-s3-devkitc-1-n16r8`. -- Build input is the exact-hash original **plus checked-in edits**, not installed source alone. Generated copies replace seven target sources without modifying SDK/managed components: HTTPS cleanup/private-key release, HTTPD scratch lifetime/null first read, ESP-TLS server-only protocol list, wolfSSH password bounds/payload wiping plus bounded IGNORE/service/string parsing, channel-window overflow rejection and ECC/Ed25519 label/exact-signature framing, DHCP option bounds (CVE-2026-45160), TLS 1.2 EMS error propagation (CVE-2026-50581), and X.509 OID allocation failure (CVE-2026-34874). The mbedTLS entries explicitly select validated nested `mbedtls`/`mbedx509` targets, not the component wrapper. Original notices and compile properties retained; all seven copies carry the 2026-09-15 modification notice; source/hash/target ambiguity fails configuration. Never hand-edit generated copies or silently repin. -- Xtensa crypto policy: root `CMakeLists.txt` sets `CURVE25519_SMALL`/`ED25519_SMALL` before component parsing; `cmake/wolf_crypto_policy.cmake` PUBLIC-propagates `cmake/wolf_crypto_policy.h` to wolfSSL consumers. PUBLIC `WOLFSSL_VALIDATE_ECC_IMPORT` and `WOLFSSL_ECDHX_SHARED_NOT_ZERO` enable existing P-256 import and X25519 all-zero-result checks. The resolved-settings guard requires these checks and both small implementations, rejects reviewed ECC validator-disabling/hardware-stub configurations, X25519 blinding and unreviewed Curve448/Ed448 enablement. Tests: `tests/wolf_crypto_policy/run.py` (strict actual production flags, real vendor crypto/ASN vectors and independently specified exact source deltas; candidate injection is not production evidence). [Key-validation review](../ssh_key_validation_review.md) records effective flags, caller/API limits and unmeasured validation cost. [Wolf review](../wolf_security_review.md) distinguishes implemented mitigations from unresolved ordering/deferred parsers; [IDF review](../idf_security_review.md) records the three backports and remaining findings. -- Parser tests: `tests/wolfssh_parser_contract/run.py`, [scope and exclusions](../../tests/wolfssh_parser_contract/README.md): 3,124 extracted-function cases per each of two stack modes, guard pages/UBSan traps and six rejected guard-removal mutations. Crypto doubles test gating, not arithmetic. PR892/881/880 subsets plus local signature framing only; no PR899 or ordering changes. Password/dispatch/deferred-source fences preserve prior contracts. -- Latest supplied parent build PASS: 94,340 B linked RAM / 1,768,949 B flash (+1,732 B versus 1,767,217 B). Strict crypto, parser, auth (135 cases), protocol policy and SDK override `--build-dir .pio/build/esp32-s3-devkitc-1-n16r8` commands PASS; independent review found no scoped blocker. Not target/runtime-reserve evidence. -- Ordering remains open: temporary PR793/819/840/855/921 attempt retained no changes; `SendNewKeys` WANT_WRITE/`SendExtInfo` continuation, `extInfoSent` rekey semantics and manual prerequisites unresolved. Registry 1.5.0/5.9.2 queries returned 404 on 2026-09-15 despite upstream tags. [Next strategy/commit pins](../wolf_security_review.md#ordering-blocker-and-actionable-next-strategy): isolated packaging/compatibility evaluation or prerequisite-audited source/header backport, with nonblocking/initial-KEX/rekey ordering tests before closure. No upgrade/device operation. +- Build input is the exact-hash original **plus checked-in edits**, not installed source alone. Generated copies replace eight target C sources plus one PUBLIC forced header without modifying SDK/managed components: HTTPS cleanup/private-key release, HTTPD scratch lifetime/null first read, ESP-TLS server-only protocol list, wolfSSH password bounds/payload wiping plus bounded IGNORE/service/string parsing, channel-window overflow rejection and ECC/Ed25519 label/exact-signature framing, DHCP option bounds (CVE-2026-45160), TLS 1.2 EMS error propagation (CVE-2026-50581), and X.509 OID allocation failure (CVE-2026-34874). The mbedTLS entries explicitly select validated nested `mbedtls`/`mbedx509` targets, not the component wrapper. Original notices and compile properties retained; all outputs carry the 2026-09-15 baseline modification notice and wolfSSH outputs add the 2026-09-16 ordering/provenance notice; source/hash/target ambiguity fails configuration. Never hand-edit generated copies or silently repin. +- Xtensa crypto policy: root `CMakeLists.txt` sets `CURVE25519_SMALL`/`ED25519_SMALL` before component parsing; `cmake/wolf_crypto_policy.cmake` PUBLIC-propagates `cmake/wolf_crypto_policy.h` to wolfSSL consumers. PUBLIC `WOLFSSL_VALIDATE_ECC_IMPORT` and `WOLFSSL_ECDHX_SHARED_NOT_ZERO` enable existing P-256 import and X25519 all-zero-result checks. The resolved-settings guard requires these checks and both small implementations, rejects reviewed ECC validator-disabling/hardware-stub configurations, X25519 blinding and unreviewed Curve448/Ed448 enablement. Tests: `tests/wolf_crypto_policy/run.py` (strict actual production flags, real vendor crypto/ASN vectors and independently specified exact source deltas; candidate injection is not production evidence). [Key-validation review](../ssh_key_validation_review.md) records effective flags, caller/API limits and unmeasured validation cost. [Wolf review](../wolf_security_review.md) distinguishes implemented restricted ordering/crypto/parser mitigations from pending target gates and deferred parsers; [IDF review](../idf_security_review.md) records the three backports and remaining findings. +- Parser tests: `tests/wolfssh_parser_contract/run.py`, [scope and exclusions](../../tests/wolfssh_parser_contract/README.md): 3,124 extracted-function cases per each of two stack modes, guard pages/UBSan traps and six rejected guard-removal mutations. Crypto doubles test gating, not arithmetic. PR892/881/880 subsets plus local signature framing only; no PR899 changes; ordering has its separate suite below. Password/dispatch/deferred-source fences preserve prior contracts. +- Latest supplied parent `pio run` PASS: **94,340 B linked RAM / 1,768,701 B flash (−248 B versus 1,768,949 B)**. Final parent **all six suites PASS**: ordering `--interop` (8,028 checks / seven rejected mutations / 12 exact-256-KiB sessions, clean channel close and transport EOF), SDK overrides `--build-dir .pio/build/esp32-s3-devkitc-1-n16r8`, auth (135), protocol, strict crypto and parser (3,124 × two modes). Interop used unsandboxed approval only for local AF_UNIX sockets; no remote network/device operation. Review's misplaced EOF guard is corrected in `SendChannelEof` before lookup/serialization/state mutation; follow-up verification found no scoped blocker. Not target/runtime-reserve evidence. +- Restricted **CVE-2025-14942** ordering correction implemented: `tools/wolfssh_order/delta.json` supplies exact executable edits to `internal.c`, `ssh.c` and `internal.h`; [README](../../tools/wolfssh_order/README.md) records PR793/819/840/855/921 prerequisite disposition, and `provenance.json` pins archived patch hashes/commit IDs. Existing X25519/P-256 KEX only; independent SELF/PEER bits, expected-message/auth gates and exactly-once queued NEWKEYS under WANT_WRITE. **EXT_INFO intentionally disabled, no `server-sig-algs`; `extInfoSent` stays zero.** No full upstream backport or dependency upgrade. CMake propagates the generated ABI header BEFORE PUBLIC plus PUBLIC forced include with stale-header rejection; joined `-include/path` flags preserve ordering/crypto guards through PlatformIO sorting/deduplication. +- Ordering tests: [tests/wolfssh_order_contract](../../tests/wolfssh_order_contract/README.md), full generated C/real wolfCrypt, **8,028 checks / seven rejected mutations**; `pio_adapter.py` validates installed PlatformIO/SCons flags with a real Xtensa consumer. Initial interop failed a harness close race despite early PASS output; fixed harness awaits peer close and transport EOF and independently reaps the server via fd-passing proxy. Final agent `--interop --interop-repeat 3`: **36/36 sessions**, exact **256 KiB** each, **two or ten** key exchanges and clean client/server exits. [Remaining gates](../wolf_security_review.md#restricted-ordering-correction-and-remaining-gates): target cleanup during rekey, no-EXT_INFO client compatibility, resource/timing and whole-phase validation; no device operation or phase sign-off. - Policy/evidence/limits: [library review](../security_library_review.md), [Phase 9C](../security_hardening.md#9c-library-cleanup-and-protocol-policy). Source-contract tests must locate and verify actual generated compilation inputs, not assume original vendor paths. ## Secure randomness diff --git a/docs/agent/current-state.md b/docs/agent/current-state.md index ade8ffb..a23c7c5 100644 --- a/docs/agent/current-state.md +++ b/docs/agent/current-state.md @@ -2,6 +2,15 @@ Working memory, not an implementation timeline. Source is authoritative; begin with [code map](code-map.md), then [architecture](architecture.md) and [decisions](design-decisions.md). +## Phase 9D continuation — restricted SSH ordering correction — 2026-09-16 + +- Initial Git status clean. Implemented restricted-profile CVE-2025-14942 correction without upgrading managed pins. `tools/wolfssh_order/` archives exact PR793/819/840/855/921 bytes, hashes/commits, authoritative consolidated delta and scope. Generator now replaces eight C sources plus one internal header. PUBLIC forced overlay and stale-header marker preserve ABI across all consumers; joined `-include/path` avoids PlatformIO argument sorting/dedup failure. +- Independent SELF/PEER keying bits, exact expected-message tracking, wrong-role/preauth/repeated-message rejection, nonblocking NEWKEYS continuation and EOF/window/exit send fences. Only current Curve25519/P256 KEX supported. Existing auth/password/parser/crypto fixes retained. **EXT_INFO deliberately disabled** in advertise/send/receive paths to eliminate skipped continuation: no server-sig-algs on wire. Tested Ed25519/P256/password OpenSSH combinations work; no RSA or broad-client compatibility claim. This is not wholesale upstream backport or unrestricted CVE closure. +- Review caught misplaced EOF guard (on exit-status); corrected real SendChannelEof and retained explicit CHANNEL_REQUEST exit guard. Added actual shutdown/pending-buffer/stale-WANT_WRITE tests, zero output/state mutation during keying. Independent recheck found no remaining scoped blockers. Initial build flag integration failure and obsolete constant warning fixed; no guards weakened. +- Parent `pio run` PASS **94,340 B linked RAM / 1,768,701 B flash** (−248 B vs prior slice). Final parent six suites PASS: ordering `--interop` (8,028 checks, seven rejected mutations, 12 sessions each exact256KiB with completed rekey/channel close/transportEOF), SDK actual build registration, auth135, protocol, strict crypto and parser3,124x2. Agent also passed36 consecutive interop sessions. Real generated wolfSSH/wolfCrypt tested with both KEX and three rekey directions; production authentication/broker integration and target timing still separate gates. +- Initial parent interop exposed harness early exit/false-early PASS before peer channel close; corrected harness independently owns server over AF_UNIX fd-passing, waits channel closure/EOF and requires both exit0 plus exact binary data. Final parent AF_UNIX test needed explicit unsandboxed approval; no remote network or device operation. Remaining suites sandboxed. +- Updated review/hardening/roadmap/license/code map with effective restricted scope and evidence. **Next:** finish remaining advisory/parser applicability and release notice/source obligations, then whole-Phase9 target validation including cleanup during rekey, no-EXT client compatibility and validation CPU/heap/stack costs. Secure boot/encrypted NVS excluded; no device operations/assets/version upgrades/commits. Phase9 remains in progress, not production sign-off. + ## Phase 9D continuation — SSH parser / key validation — 2026-09-15 - Initial Git status clean; previous 9D work already retained. Current slice keeps managed pins and seven-source override mechanism unchanged. `tools/security_overrides.py` now bounds IGNORE/service/helper parsing, rejects window-add overflow, fixes ECC/Ed25519 label predicates, and enforces ECC nested r/s plus outer signature exact consumption and Ed25519 exact signature-field consumption. Password wipe/async and state ordering unchanged. diff --git a/docs/dependency_licenses.md b/docs/dependency_licenses.md index ef2df36..b9b3746 100644 --- a/docs/dependency_licenses.md +++ b/docs/dependency_licenses.md @@ -1,10 +1,10 @@ # Dependency license inventory — Phase 9D -## Implementation addendum — 2026-09-15 +## Implementation addendum — 2026-09-16 -**Missing generated modification/date notices: resolved for all seven generated sources.** `tools/security_overrides.py` prepends a prominent project modification notice dated **2026-09-15**, identifying the editable generator and warning against editing derived copies. Read-only verification matched every existing generated file to its pinned original plus checked-in edits and notice. Both new mbedTLS copies retain their original Mbed TLS Contributors copyright and **`Apache-2.0 OR GPL-2.0-or-later`** SPDX header; the Apache-2.0 selection in this inventory is unchanged. This resolves the narrow notice-generation finding, **not release packaging or license clearance**. +**Missing generated modification/date notices: resolved; current count is eight C sources plus one header.** `tools/security_overrides.py` prepends the baseline project modification notice dated **2026-09-15**, identifying the editable generator and warning against editing derived copies. The wolfSSH `internal.c`, added `ssh.c` and generated `wolfssh/internal.h` also receive a prominent **2026-09-16** ordering-profile/provenance notice. Exact original hashes and exact-once edits remain required; upstream notices are retained. The header is a PUBLIC forced build input, not a ninth C source. Both new mbedTLS copies retain their original Mbed TLS Contributors copyright and **`Apache-2.0 OR GPL-2.0-or-later`** SPDX header; the Apache-2.0 selection in this inventory is unchanged. This resolves the narrow notice-generation finding, **not release packaging or license clearance**. -Radio-blob corresponding-source/exception questions, the actual firmware/device/browser notice bundle and delivery, preferred-source packaging, wolfSSH license-package discrepancy, icon provenance/full-license work, and release-specific runtime/bootloader review remain open. Include the new `cmake/wolf_crypto_policy.{cmake,h}` policy inputs with the reproducible build/source package. No upgrades, asset regeneration or device operations were performed for this update. +Radio-blob corresponding-source/exception questions, the actual firmware/device/browser notice bundle and delivery, preferred-source packaging, wolfSSH license-package discrepancy, icon provenance/full-license work, and release-specific runtime/bootloader review remain open. Include `cmake/wolf_crypto_policy.{cmake,h}` and the ordering inputs described below with the reproducible build/source package. No upgrades, asset regeneration or device operations were performed for this update. ## Historical inventory baseline — 2026-09-15 @@ -99,21 +99,37 @@ For conveyance, review GPLv3 §§1, 4–6, 7 and 10 in [LICENSE](../LICENSE): 1. Supply required license/copyright/warranty notices and use a §6-compliant corresponding-source delivery method for binaries. A dependency name, registry URL, generic upstream source link, or this inventory alone is not corresponding source or a compliant written offer. 2. Preserve exact application source, lock/manifests, relevant SDK and managed source, configuration/build/link inputs, asset preferred sources and generation scripts. Include the scripts controlling compilation/installation. Generic unmodified build tools may fall outside corresponding source, but record precise versions needed for reproduction; if distributing those tools themselves, satisfy their separate licenses. -3. The Phase 9C firmware compiles **pinned originals plus project edits**. Include `tools/security_overrides.py`, `cmake/security_overrides.cmake`, root CMake integration, original hash-matching sources, and the means to reproduce the generated copies. A stock IDF/wolfSSH tarball alone omits the compiled modifications. Generated copies alone are not a substitute for the preferred editable patch/generator inputs. -4. **Resolved 2026-09-15:** all seven generated files below retain upstream notices and now carry the explicit project modification/date notice from the generator. The historical four-file audit found the notice missing; this narrow defect is repaired. GPLv3 §5(a) calls for a relevant date, and Apache-2.0 §4(b) requires prominent change notices. Continue including notices and reproducible generator inputs in distribution; do not hand-edit generated copies or vendor sources. +3. The Phase 9C–9D firmware compiles **pinned originals plus project edits**. Include `tools/security_overrides.py`, `cmake/security_overrides.cmake`, root CMake integration, `cmake/wolf_crypto_policy.{cmake,h}`, the complete `tools/wolfssh_order/` inputs, original hash-matching C/header sources, and the means to reproduce all eight C copies and the PUBLIC forced header. A stock IDF/wolfSSH tarball alone omits the compiled modifications. Generated copies alone are not a substitute for the preferred editable patch/generator inputs. +4. **Resolved; updated 2026-09-16:** all eight generated C files plus one header below retain upstream notices and carry explicit project modification/date notices from the generator (baseline 2026-09-15, plus 2026-09-16 ordering notices on wolfSSH outputs). The historical four-file audit found the notice missing; this narrow defect is repaired. GPLv3 §5(a) calls for a relevant date, and Apache-2.0 §4(b) requires prominent change notices. Continue including notices and reproducible generator inputs in distribution; do not hand-edit generated copies or vendor sources. 5. If distributing a GPLv3 “User Product,” assess and provide required Installation Information under §6. Do not publish device passwords, private keys, Wi-Fi material, account verifiers, tickets, NVS images, or real credential backups as build/source artifacts. Installation compliance should use appropriate documented procedures, not routine secret disclosure. Secure boot was not enabled or altered here. 6. **Prebuilt radio libraries are the principal unresolved compatibility/source risk.** Apache-2.0 permits binary redistribution, but does not itself satisfy GPL corresponding-source obligations for a combined firmware. Establish whether required preferred source is obtainable, or whether a defensible GPL System Library/other exception applies to the actual linked artifacts. Static linkage is not automatically “mere aggregation.” Do not declare distribution cleared or definitely prohibited from this bounded audit. Escalate this before a public binary/device release. -Checked generated paths and retained licenses: +Current generated paths and retained licenses (eight C sources plus one header): - `BUILD/security_overrides/https_server/https_server.c` — Espressif Apache-2.0. - `BUILD/security_overrides/httpd_parse/httpd_parse.c` — Espressif Apache-2.0. - `BUILD/security_overrides/esp_tls_mbedtls/esp_tls_mbedtls.c` — Espressif Apache-2.0. - `BUILD/security_overrides/wolfssh_internal/internal.c` — wolfSSH GPL-3.0-or-later header. +- `BUILD/security_overrides/wolfssh_ssh/ssh.c` — wolfSSH GPL-3.0-or-later header. +- `BUILD/security_overrides/wolfssh_include/wolfssh/internal.h` — wolfSSH GPL-3.0-or-later header; ABI overlay propagated BEFORE PUBLIC and as a PUBLIC forced include, not a C compilation unit. - `BUILD/security_overrides/dhcpserver/dhcpserver.c` — retained Espressif Apache-2.0 header. - `BUILD/security_overrides/mbedtls_ssl_tls/ssl_tls.c` — retained Mbed TLS Contributors, `Apache-2.0 OR GPL-2.0-or-later`. - `BUILD/security_overrides/mbedtls_x509_create/x509_create.c` — retained Mbed TLS Contributors, `Apache-2.0 OR GPL-2.0-or-later`. +### Archived ordering patches and license context + +[Ordering provenance](../tools/wolfssh_order/README.md) documents the restricted CVE-2025-14942 correction, not a complete upstream backport or upgrade. [`delta.json`](../tools/wolfssh_order/delta.json) is the preferred editable, executable consolidated delta; archived upstream mail patches are provenance/prerequisite evidence, not a statement that all their hunks compile into the firmware. [`provenance.json`](../tools/wolfssh_order/provenance.json) retains the original URLs and every full embedded commit ID. Archived bytes fetched 2026-09-16 are pinned as follows; future PR responses may differ: + +| Archive under `tools/wolfssh_order/` | SHA-256 | +| --- | --- | +| `793.patch` | `66aa999521800371f97b340db2bff162be4e8ffab4b4b953f9d32b8e33f56cea` | +| `819.patch` | `8e63c2b24679a7d831f7dba12e412d2f8a7fdc391fc4e8fd33f029487110219b` | +| `840.patch` | `23e25873cb4dfa36063357111019e0960dfc008a68cc29d6e2d5c37bde47ec94` | +| `855.patch` | `b94393df9528f66f6df1aba94015331fee3110c9ea2007a0eff369d8887b4b29` | +| `921.patch` | `f6c88598d7f3c94d92c31cecc4ef1e6d779b49f9eff6556ac1c57426d272d504` | + +Preserve upstream mail authorship, commit metadata and applicable file notices with these archives. They contain upstream wolfSSH code/context, not newly project-authored material automatically relicensed by the generator's `GPL-3.0-only` SPDX line. The compiled wolfSSH C/header derivatives retain the upstream **GPL-3.0-or-later** grants; select GPLv3 for this integration. Patch hashes prove identity, not license permission or full archive-level clearance. Retain the upstream license texts and package discrepancy evidence, and review applicable per-file terms for any archived but unapplied example/test/workflow content when assembling the source package. No standalone blanket license grant for every patch byte is inferred from mail-patch format. Source/notice delivery and release packaging remain open. + For Apache-2.0 components, retain required attribution and any applicable upstream NOTICE contents under §4(d), **if present**; do not fabricate NOTICE text or assert every Apache package must have a NOTICE file. For MIT/BSD components, distribute the actual copyright, permission/conditions and disclaimer, not merely an SPDX name. Preserve mixed notices such as argtable3 and Newlib rather than replacing them with generic templates. ## Actionable release work (not performed) @@ -123,7 +139,7 @@ For Apache-2.0 components, retain required attribution and any applicable upstre | Release blocker to resolve | Document a defensible corresponding-source/exception basis for the **selected radio blobs** and other source-unavailable linked content. This is an unresolved question, not a legal verdict. | | Before distributing | Assemble a versioned third-party notice bundle from the **actual resolved packages**: GPL texts/options, Apache text and applicable NOTICE/attribution, MIT/BSD notices, mixed libc/console texts, and GCC exception. Validate firmware/device/browser delivery, not just Git presence. Include TLSF's correct full copyright/conditions. | | Before distributing icon derivatives | Obtain the full Apache-2.0 text and confirm exact two-icon provenance/licensing at 7.4.47; retain upstream attribution and any applicable NOTICE. Existing source comments already identify adaptations. Do not invent an absent upstream NOTICE or copy unrelated icon licenses. | -| Resolved in generator; retain in distribution | Prominent modification notices dated **2026-09-15** now appear in **all 7 generated files**, with original notices retained. Release packaging must still include them and the reproducible patch/build inputs. | +| Resolved in generator; retain in distribution | Prominent modification notices cover **eight C sources plus one header** (baseline **2026-09-15**, wolfSSH ordering **2026-09-16**), with original notices retained. Release packaging must still include them, archived patches/provenance, consolidated delta and reproducible build inputs. | | Upstream clarification | Record/resolve wolfSSH 1.4.20 package LICENSE/README vs source-header mismatch without editing the managed package in place. GPLv3 is supported by the inspected headers and is also within the package prose's “or later” wording. | | Release packaging verification | Archive exact corresponding source/build inputs and required asset preferred sources; validate source delivery/offer and any User Product installation information. Recheck bootloader, runtime objects and aggregation boundaries against the final release map. | diff --git a/docs/roadmap.md b/docs/roadmap.md index a076ac9..c047c26 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -216,7 +216,7 @@ Staged work: 1. **9A — Crash/debug build policy and operational profiles — In progress; hardware pending.** `src/security_build_policy.c` requires `CONFIG_ESP_COREDUMP_ENABLE_TO_NONE=y` and `CONFIG_ESP_SYSTEM_PANIC_SILENT_REBOOT=y`; rejects core-dump enable/flash/UART, panic print/halt/GDBstub, runtime GDBstub and ESP/FreeRTOS debugger-aware options. `sdkconfig.defaults` makes the baseline explicit. Development/test/production use the same build baseline, not separate PlatformIO environments. Host matrix (`python3 tests/security_build_policy/run.py`) compiles the actual guard: 17 cases plus the generated-header check passed on 2026-09-15. `pio run` passed (94,196 B linked RAM / 1,828,565 B flash); target panic/recovery tests have not run. Production readiness remains pending. 2. **9B — SSH admission and credential handling — Implemented; combined target validation deferred.** Boot-lifetime, owner-only token buckets independently bound handshakes, password/signed-key requests and unsigned probes; reconnect/restart/counter clearing do not replenish them. Existing per-slot attempt limits/currentness remain. Explicit keyboard-interactive rejection, pending-signature result fencing, secret-free admission counters, consumed admin-buffer wipes and fail-closed hidden-prompt overflow/unsupported-byte handling are implemented. Four focused suites (including 35 pinned-vendor control-flow cases), 11 related regressions and `pio run` passed on 2026-09-15: 94,340 B linked RAM / 1,829,925 B flash. Global-budget starvation remains a documented tradeoff, not a solved availability problem. 3. **9C — Library cleanup and protocol policy — Implemented; combined target validation deferred.** Exact-hash build-tree overrides correct HTTPS cleanup/leaks, HTTPD scratch failure/wiping/first-read handling, bounded SSH password parsing/wiping and server-local TLS policy without modifying installed dependencies. Secure wolfSSL allocation hooks and explicit SSH policy fail closed; early web quota probing avoids receiving already-throttled bodies. TLS1.2 ECDHE-ECDSA AES-GCM and SSH GCM/modern-KEX allowlists intentionally exclude legacy-only clients; no identity migration. Bounded password/certificate/header/destructor review is documented, not exhaustive zeroization. Final build PASS 94,340 B linked RAM / 1,831,309 B flash; focused and related host/source-contract tests passed. [Review and maintenance contract](security_library_review.md). -4. **9D — Maintenance and lifecycle — In progress; unresolved security and distribution questions.** [Security operations](security_operations.md) documents provisioning, explicit saves, rotation, recovery, reconstruction (no implemented backup/export) and approved destructive reset/decommissioning. The [wolfSSL/wolfSSH implementation addendum](wolf_security_review.md) records the implemented Xtensa small X25519/Ed25519 mitigation (CVE-2025-12888), with consistent library/consumer flags and a resolved-settings guard; small X25519 is not combined with blinding. Bounded IGNORE/service/string parsing, channel-window overflow, ECC/Ed25519 labels and exact signature framing are now corrected in the existing generated override. [P-256 import and X25519 all-zero-result checks](ssh_key_validation_review.md) are enabled with effective PUBLIC flags confirmed; generic digest/OID API hardening and deferred parser semantics remain open. **Server-recommended ordering work (CVE-2025-14942) is not fixed:** a temporary PR793/819/840/855/921 backport attempt retained no changes because manual prerequisites, nonblocking `SendNewKeys`/`WS_WANT_WRITE` → `SendExtInfo` continuation and `extInfoSent` rekey semantics remain unresolved. The [IDF implementation addendum](idf_security_review.md) records pinned DHCP (CVE-2026-45160), TLS 1.2 EMS (CVE-2026-50581) and X.509 allocation-failure (CVE-2026-34874) backports; historical research remains labeled and retained. Supplied parent build PASS: **94,340 B linked RAM / 1,768,949 B flash**, unchanged RAM / **+1,732 B flash** versus the preceding 1,767,217 B build. All five parent commands passed: strict wolf-crypto-policy, wolfSSH-parser-contract (3,124 cases per each of two modes plus six rejected mutations), wolfSSH-auth-contract (135 cases), SSH-protocol-policy and SDK-override with actual build-directory registration. Independent review found no blocker in these scoped changes; see [evidence and limits](security_hardening.md#mitigation-hostbuild-evidence--2026-09-15). The [bounded license inventory](dependency_licenses.md) marks modification/date notices resolved for all seven generated files (2026-09-15; upstream licenses retained), but radio-blob corresponding-source/exception, source/notice delivery and packaging/provenance questions remain open. Official registry wolfSSH 1.5.0 / wolfSSL 5.9.2 queries returned 404 on 2026-09-15; upstream tags exist but managed compatibility is not established. The [next strategy and immutable commit references](wolf_security_review.md#ordering-blocker-and-actionable-next-strategy) call for isolated upstream packaging/compatibility evaluation or a prerequisite-audited coherent backport, with partial-send/EXT_INFO/rekey and negative ordering tests. Upgrades/backports require coherent source/header review, override rebasing and contract tests, not blind repinning. Broader dependency advisory coverage remains unfinished. No dependency upgrade was performed. **Phase 9 is not complete or production-ready; scoped mitigations do not establish full advisory closure, device validation or license/distribution clearance.** OTA signing trust remains separate Phase 10 work. +4. **9D — Maintenance and lifecycle — In progress; unresolved security and distribution questions.** [Security operations](security_operations.md) documents provisioning, explicit saves, rotation, recovery, reconstruction (no implemented backup/export) and approved destructive reset/decommissioning. The [wolfSSL/wolfSSH implementation addendum](wolf_security_review.md) records the implemented Xtensa small X25519/Ed25519 mitigation (CVE-2025-12888), with consistent library/consumer flags and a resolved-settings guard; small X25519 is not combined with blinding. Bounded IGNORE/service/string parsing, channel-window overflow, ECC/Ed25519 labels and exact signature framing are now corrected in the existing generated override. [P-256 import and X25519 all-zero-result checks](ssh_key_validation_review.md) are enabled with effective PUBLIC flags confirmed; generic digest/OID API hardening and deferred parser semantics remain open. **Restricted existing-profile ordering correction (CVE-2025-14942) is implemented**, using audited PR793/819/840/855/921 subsets plus local gates, not a full upstream backport. X25519/P-256 only, independent SELF/PEER bits, expected-message/authentication gates and queued NEWKEYS continuation handle nonblocking writes. **EXT_INFO is intentionally disabled; no `server-sig-algs` is advertised.** Eight C overrides plus one PUBLIC forced ABI header retain hash pins/notices; joined forced-header arguments fix PlatformIO sorting/deduplication. The review's misplaced EOF guard is corrected and verified with no scoped blocker. The [IDF implementation addendum](idf_security_review.md) records pinned DHCP (CVE-2026-45160), TLS 1.2 EMS (CVE-2026-50581) and X.509 allocation-failure (CVE-2026-34874) backports; historical research remains labeled and retained. Supplied parent build PASS: **94,340 B linked RAM / 1,768,701 B flash**, unchanged RAM / **−248 B flash** versus 1,768,949 B. Final parent **all six suites PASS**: ordering `--interop` (8,028 checks, seven rejected mutations, 12 exact-256-KiB sessions with clean channel close/transport EOF), SDK overrides with actual `--build-dir` registration, auth (135), protocol, strict crypto and parser (3,124 × two modes). Interop required unsandboxed approval only for local AF_UNIX sockets; no remote network/device operation. Agent ordering evidence: **8,028 checks / seven rejected mutations**. Initial interop failed a harness close race despite early PASS output; the corrected harness waits for peer close/transport EOF and independently reaps the server through an fd-passing proxy. Final agent repeated OpenSSH matrix: **36/36 sessions**, each exact **256 KiB**, **two or ten** completed key exchanges and clean client/server exits; see [evidence and limits](security_hardening.md#ordering-hostbuild-evidence--2026-09-16). The [bounded license inventory](dependency_licenses.md) marks modification/date notices resolved for all eight generated C files plus one header (baseline 2026-09-15 and wolfSSH ordering 2026-09-16; upstream licenses retained), with archived patch hashes/commits and patch license context recorded, but radio-blob corresponding-source/exception, source/notice delivery and packaging/provenance questions remain open. Official registry wolfSSH 1.5.0 / wolfSSL 5.9.2 queries returned 404 on 2026-09-15; upstream tags exist but managed compatibility is not established. The [remaining ordering gates and immutable provenance](wolf_security_review.md#restricted-ordering-correction-and-remaining-gates) retain target cleanup during rekey, intended-client compatibility without EXT_INFO and whole-phase resource/timing validation. Future upstream adoption still requires isolated packaging/compatibility evaluation. Upgrades/backports require coherent source/header review, override rebasing and contract tests, not blind repinning. Broader dependency advisory coverage remains unfinished. No dependency upgrade was performed. **Phase 9 is not complete or production-ready; scoped mitigations do not establish full advisory closure, device validation or license/distribution clearance.** OTA signing trust remains separate Phase 10 work. At the user's request, hardware validation is deferred to **Phase 9 as a whole**, not required between implementation slices. [Security hardening](security_hardening.md) collects profiles, host evidence and the combined target checklist. Silent panic reboot removes useful crash diagnostics, not ordinary reset/boot/status information or every possible log disclosure. Raw flash/RAM/dumps remain secret-bearing, not routine diagnostic exports. Existing coredump bytes are not retroactively cleared; no secure erase is claimed. Isolated synthetic-secret debug builds require explicit reviewed source-policy changes, not a provided bypass flag. diff --git a/docs/security_hardening.md b/docs/security_hardening.md index a5b8995..9fbd191 100644 --- a/docs/security_hardening.md +++ b/docs/security_hardening.md @@ -71,7 +71,7 @@ Hidden UART0 and shared remote-console prompts now reject overflow or unsupporte ### Reproducible source corrections -`tools/security_overrides.py` verifies full original-file SHA-256 values and ESP-IDF 5.5.0, applies exact-once edits, and generates seven corrected sources under the build directory (four from 9C plus three advisory backports in 9D). `cmake/security_overrides.cmake`, included after `project()`, replaces exactly the corresponding sources in existing IDF/component targets, retaining compilation properties. Installed SDK/managed sources and their notices remain unchanged. Missing, changed or ambiguous sources fail configuration; there is no unpatched fallback. Do not edit derived files or repin a hash merely to make an upgrade build. +`tools/security_overrides.py` verifies full original-file SHA-256 values and ESP-IDF 5.5.0, applies exact-once edits, and generates eight corrected C sources plus one header under the build directory (the original four 9C sources, three IDF advisory sources, and the additional wolfSSH `ssh.c`/`internal.h` ordering inputs). `cmake/security_overrides.cmake`, included after `project()`, replaces exactly the corresponding sources in existing IDF/component targets, retaining compilation properties. Installed SDK/managed sources and their notices remain unchanged. Missing, changed or ambiguous sources fail configuration; there is no unpatched fallback. Do not edit derived files or repin a hash merely to make an upgrade build. - **HTTPS:** delete TLS on post-handshake transport-allocation failure; fully destroy retained TLS configuration on failed HTTPD start; wipe the copied raw private key before free. Failed stop still retains live ownership. - **HTTPD parser:** allocate/copy/wipe/free scratch on resize, preserve the old pointer on allocation failure, wipe final scratch, and handle the null initial parser pointer without undefined subtraction. Pending/unread bytes retain their existing behavior. @@ -90,11 +90,11 @@ The new `src/ssh_memory.{c,h}` wolfSSL/wolfCrypt allocation hooks wipe the full | SSH host key | `ecdsa-sha2-nistp256` | | SSH ciphers, both directions | `aes128-gcm@openssh.com`, `aes256-gcm@openssh.com` | | SSH MAC advertisement | `hmac-sha2-256` (GCM supplies packet authentication) | -| SSH user-key advertisement | `ssh-ed25519`, `ecdsa-sha2-nistp256`; database authorization remains authoritative | +| SSH user-key policy | `ssh-ed25519`, `ecdsa-sha2-nistp256`; database authorization remains authoritative; no EXT_INFO/`server-sig-algs` advertisement | `src/ssh_protocol_policy.c` uses permanent strings and checks every setter; any failure destroys the unpublished context without fallback. Tests verify the actual compiler's available algorithms and generated KEXINIT lists, not merely setter success. The server-only TLS correction avoids breaking future outbound HTTPS clients by globally removing RSA-certificate suites. -**Compatibility:** TLS CBC/CCM/ARIA-only and SSH CBC/CTR-only clients no longer connect; removed KEX-only clients also fail. Mainstream-client interoperability and rekey remain target gates, not host-test claims. Existing TLS/SSH identity and user-key storage need no rotation or migration. Password/KDF, certificate validity/trust and browser-header policy were reviewed and retained with documented limits; no blind KDF-cost increase or HSTS policy was introduced. +**Compatibility:** TLS CBC/CCM/ARIA-only and SSH CBC/CTR-only clients no longer connect; removed KEX-only clients also fail. Bounded OpenSSH host interoperability is recorded below; intended-client compatibility without EXT_INFO and rekey/cleanup on hardware remain target gates. RSA-SHA2 discovery is not claimed. Existing TLS/SSH identity and user-key storage need no rotation or migration. Password/KDF, certificate validity/trust and browser-header policy were reviewed and retained with documented limits; no blind KDF-cost increase or HSTS policy was introduced. ### Web admission and shorter plaintext lifetimes @@ -105,10 +105,10 @@ The existing global five-verifications/60-second fixed window is unchanged: malf ## 9D maintenance and lifecycle — in progress - [Security operations](security_operations.md) supplies source-checked provisioning, explicit-save, account/key/identity rotation, recovery, reconstruction and approved erase/decommissioning procedures. There is no implemented configuration backup/export or private-identity restore workflow. Commands and checklists are not device execution evidence; the user will validate Phase 9 as a whole. -- [wolfSSL/wolfSSH review and implementation addendum](wolf_security_review.md): the **CVE-2025-12888** Xtensa mitigation now selects `CURVE25519_SMALL` and `ED25519_SMALL` consistently for wolfSSL and consumers. The PUBLIC forced-include resolved-settings guard rejects missing small implementations, X25519 blinding (incompatible with small in this pinned version), and unreviewed Curve448/Ed448 enablement. The existing generated wolfSSH override now bounds IGNORE/service strings, rejects zero-capacity string output and channel-window overflow, corrects ECC/Ed25519 key/signature labels, and enforces exact signature framing (ECC nested r/s bounds plus inner/outer consumption; Ed25519 outer consumption). These are PR892/881/880 subsets plus local framing corrections, not full backports; password wiping/async retention remain unchanged. PUBLIC `WOLFSSL_VALIDATE_ECC_IMPORT` and `WOLFSSL_ECDHX_SHARED_NOT_ZERO` now enable P-256 import validation and X25519 all-zero-result rejection, with effective production flags confirmed and fail-closed backend guards. See [key-validation evidence](ssh_key_validation_review.md) and [parser scope/limits](../tests/wolfssh_parser_contract/README.md). **Still open:** server-recommended message-order correction for **CVE-2025-14942**, PR899/deferred parsers, service/key-blob semantics and broader API applicability. Current raw SSH digest construction does not expose the reviewed short-digest/OID trigger; generic PR10131 API hardening is not backported. No exploit or whole-library clearance is demonstrated; added validation CPU/allocation cost and target interoperability remain unmeasured. +- [wolfSSL/wolfSSH review and implementation addendum](wolf_security_review.md): the **CVE-2025-12888** Xtensa mitigation now selects `CURVE25519_SMALL` and `ED25519_SMALL` consistently for wolfSSL and consumers. The PUBLIC forced-include resolved-settings guard rejects missing small implementations, X25519 blinding (incompatible with small in this pinned version), and unreviewed Curve448/Ed448 enablement. The existing generated wolfSSH override now bounds IGNORE/service strings, rejects zero-capacity string output and channel-window overflow, corrects ECC/Ed25519 key/signature labels, and enforces exact signature framing (ECC nested r/s bounds plus inner/outer consumption; Ed25519 outer consumption). These are PR892/881/880 subsets plus local framing corrections, not full backports; password wiping/async retention remain unchanged. PUBLIC `WOLFSSL_VALIDATE_ECC_IMPORT` and `WOLFSSL_ECDHX_SHARED_NOT_ZERO` now enable P-256 import validation and X25519 all-zero-result rejection, with effective production flags confirmed and fail-closed backend guards. See [key-validation evidence](ssh_key_validation_review.md) and [parser scope/limits](../tests/wolfssh_parser_contract/README.md). The restricted existing-profile correction for **CVE-2025-14942** is implemented as described below. **Still open:** PR899/deferred parsers, service/key-blob semantics and broader API applicability. Current raw SSH digest construction does not expose the reviewed short-digest/OID trigger; generic PR10131 API hardening is not backported. No exploit or whole-library clearance is demonstrated; added validation CPU/allocation cost and target interoperability remain unmeasured. - [Focused IDF review and implementation addendum](idf_security_review.md): pinned backports now implement DHCP option bounds (**CVE-2026-45160**), TLS 1.2 EMS failure return (**CVE-2026-50581**) and X.509 OID allocation-failure handling (**CVE-2026-34874**). Explicit nested-target validation places the mbedTLS edits on `mbedtls`/`mbedx509`, retaining source properties and exactly-one-source checks. WS negotiation **CVE-2026-45541** and ASN.1 named-data **CVE-2025-48965** remain unpatched with the review's qualified applicability, not blanket closure. -- [Dependency license inventory](dependency_licenses.md) is a bounded engineering inventory, not legal or distribution clearance. All seven generated sources now carry prominent modification notices dated **2026-09-15**, with upstream notices retained, including both mbedTLS dual-license headers. That narrow finding is resolved. Radio-blob corresponding-source/exception questions, actual firmware/device/browser notice delivery, preferred-source packaging, wolfSSH package-license discrepancy and icon provenance/full-license work remain open. Existing repository notices alone are not a complete release bundle. -- **Ordering is not fixed.** A temporary coherent PR793/819/840/855/921 backport attempt retained no changes: manual prerequisites and nonblocking `SendNewKeys`/`WS_WANT_WRITE` skipping `SendExtInfo` continuation, plus `extInfoSent` rekey semantics, remain unresolved. Official registry 1.5.0/5.9.2 queries returned 404 on 2026-09-15 despite upstream tags existing. Follow the [actionable ordering strategy](wolf_security_review.md#ordering-blocker-and-actionable-next-strategy): evaluate immutable upstream snapshots with reviewed packaging or audit a complete prerequisite-aware backport; require partial-send/EXT_INFO/initial-KEX/rekey and negative ordering tests before closure. +- [Dependency license inventory](dependency_licenses.md) is a bounded engineering inventory, not legal or distribution clearance. All **eight generated C sources plus one header** carry prominent modification notices: the baseline **2026-09-15** notice plus **2026-09-16** ordering/provenance notices on wolfSSH outputs, with upstream notices retained, including both mbedTLS dual-license headers. That narrow finding is resolved. Radio-blob corresponding-source/exception questions, actual firmware/device/browser notice delivery, preferred-source packaging, wolfSSH package-license discrepancy and icon provenance/full-license work remain open. Existing repository notices alone are not a complete release bundle. +- **Restricted ordering correction implemented, not a full upstream backport or sign-off.** The [ordering review](wolf_security_review.md#restricted-ordering-correction-and-remaining-gates) and [provenance/prerequisite disposition](../tools/wolfssh_order/README.md) document the audited PR793/819/840/855/921 subsets plus local gates. Existing X25519/P-256 KEX only; independent SELF/PEER state, exact expected replies and authentication-phase checks cover both roles. Queued NEWKEYS survives WANT_WRITE without duplication. **EXT_INFO is deliberately disabled; no `server-sig-algs` is sent**, and `extInfoSent` stays zero. CMake applies the generated ABI header BEFORE PUBLIC and via a PUBLIC forced include; joined `-include/path` flags fix PlatformIO sorting/deduplication for ordering and crypto guards. Review's misplaced EOF guard is corrected before channel mutation; verification found no scoped blocker. Target cleanup during rekey and no-EXT_INFO client compatibility remain pending. - No dependency versions were upgraded. Beyond the implemented backports above, proposed upgrades/backports remain candidates, not approved compatible versions. Re-audit coherent source/header changes, effective compile policy, exact-hash overrides and callback/parser contracts, then obtain host/build and whole-phase target evidence. 9A–9C passes below are historical scoped evidence, not closure of these newly recorded findings. Secure boot and encrypted NVS remain excluded. No runbook, advisory report or license inventory establishes production readiness or authorizes a destructive device operation. @@ -129,6 +129,14 @@ Raw flash, RAM and dumps can contain Wi-Fi passwords, private keys, password ver ## Validation gates +### Ordering host/build evidence — 2026-09-16 + +Supplied parent `pio run` **PASS**: **94,340 B linked RAM / 1,768,701 B flash**, unchanged RAM / **−248 B flash** versus 1,768,949 B. **Supplied final parent results: all six suites PASS** — ordering `--interop` (8,028 checks, seven rejected mutations, 12 sessions with exact 256 KiB echo each and clean channel close plus transport EOF), SDK overrides `--build-dir .pio/build/esp32-s3-devkitc-1-n16r8`, auth (135 cases), protocol, strict crypto, and parser (3,124 cases in each of two stack modes). Interop required unsandboxed approval solely for local AF_UNIX sockets; no remote network or device operation occurred. This documentation update did not rerun firmware or host suites. + +Supplied agent ordering tests passed **8,028 checks and seven rejected mutations**, including the corrected EOF guard and real shutdown/exit-status rekey fences. The installed PlatformIO/SCons adapter regression validates joined forced-header flags with a real Xtensa consumer and rejects a split-option mutation. The [test README and code](../tests/wolfssh_order_contract/README.md) describe full generated translation units, real wolfCrypt, message-ID matrices, fragmented writes and both roles/rekey directions. + +Initial host interoperability failed a harness close race: early `INTEROP PASS` preceded OpenSSH `Broken pipe` and was not a pass. The harness now waits for peer channel close and transport EOF, passes a local socket descriptor to OpenSSH, independently owns/reaps the server, and checks both process exits. Final agent `python3 tests/wolfssh_order_contract/run.py --interop --interop-repeat 3` evidence: **36/36 sessions**, each exact **256 KiB** binary echo, **ten** key exchanges in client-rekey cases or **two** in fragmented server-rekey cases, clean exits and no EXT_INFO. Coverage uses OpenSSH 10.2p1, both KEX algorithms, Ed25519/P-256/password authentication and AES128-GCM. It is not general library shutdown, arbitrary-client or target evidence. Whole-phase gates remain pending. + ### Mitigation host/build evidence — 2026-09-15 Supplied parent results (not rerun for this documentation update): `pio run` **PASS**, **94,340 B linked RAM / 1,768,949 B flash**. RAM is unchanged and flash is **1,732 B larger** than the preceding 1,767,217 B mitigation build. Linked size is not runtime headroom or timing evidence. @@ -169,7 +177,7 @@ Record the revision, compiler/build outcome and effective configuration. Confirm ### Combined Phase 9 target validation — deferred, not run -Retain these checks for the user's final whole-phase test session; do not stop implementation for separate slice sign-off. Include the [9D operational rehearsal](security_operations.md#deferred-whole-phase-9-hardware-rehearsal) and targeted message-order/parser/key-validation/interoperability checks for the eventual reviewed advisory mitigations. None is recorded as passed here. +Retain these checks for the user's final whole-phase test session; do not stop implementation for separate slice sign-off. Include the [9D operational rehearsal](security_operations.md#deferred-whole-phase-9-hardware-rehearsal) and targeted message-order/parser/key-validation/interoperability checks for the implemented restricted mitigations and any subsequent reviewed changes. None is recorded as passed here. #### Crash and recovery @@ -190,7 +198,7 @@ Retain these checks for the user's final whole-phase test session; do not stop i #### Protocol compatibility and allocation-failure recovery -1. Verify both allowed TLS suites and both SSH GCM ciphers using compatible clients; force excluded CBC/CTR/other-only offers and confirm rejection. Exercise both SSH KEX choices and both user-key types, initial handshake and rekey, plus TLS renegotiation rejection. Retain UART0 access; do not rotate identities to work around an algorithm mismatch. +1. Verify both allowed TLS suites and both SSH GCM ciphers using compatible clients; force excluded CBC/CTR/other-only offers and confirm rejection. Exercise both SSH KEX choices and both user-key types, initial handshake and rekey, plus TLS renegotiation rejection. Verify intended clients work without EXT_INFO/`server-sig-algs`; do not assume RSA-SHA2 discovery. Exercise disconnect/cleanup during rekey and subsequent session recovery under load. Retain UART0 access; do not rotate identities to work around an algorithm mismatch. 2. With synthetic credentials, test truncated/oversized SSH password and change-password packets: no authentication callback for malformed fields, no crash, bounded disconnect/recovery. Include malformed IGNORE/service strings, window overflow, ECC/Ed25519 labels and nested/trailing signature bytes, invalid P-256 points and low-order X25519 inputs. Measure added import-validation latency/allocation/stack cost, host-key loading and handshake deadlines under repeated KEX/rekey and combined load. Host canary/vector assertions are not real encrypted-packet coverage. 3. Exercise HTTPS failed-start, post-handshake allocation failure, normal/failed-stop retry and split-header scratch allocation failure on a separately reviewed fault-injection image. Observe recovery/no accumulating allocation loss without exporting keys or RAM. Failed stop must not prematurely free live TLS state. 4. Repeatedly start/stop HTTPS and SSH and stress header parsing/authentication under the full transport mix. Capture internal/DMA/PSRAM free/minimum/largest-block and stack margins alongside serial/broker loss counters. Specifically measure old-plus-new allocation peaks and secure-free CPU cost; previous very low internal minima remain important. @@ -198,7 +206,7 @@ Retain these checks for the user's final whole-phase test session; do not stop i ## Staged next work -- **Continue 9D maintenance and lifecycle.** Execute the [ordering strategy and remaining advisory work](wolf_security_review.md#ordering-blocker-and-actionable-next-strategy), finish broader dependency coverage beyond the bounded IDF/mbedTLS review and three implemented backports, and address the [release source/notice work](dependency_licenses.md#actionable-release-work-not-performed). Runbooks are documented, not rehearsed; remaining mitigations/reviews, distribution clearance and whole-phase acceptance remain outstanding. +- **Continue 9D maintenance and lifecycle.** Execute the [remaining ordering gates and advisory work](wolf_security_review.md#restricted-ordering-correction-and-remaining-gates), finish broader dependency coverage beyond the bounded IDF/mbedTLS review and three implemented backports, and address the [release source/notice work](dependency_licenses.md#actionable-release-work-not-performed). Runbooks are documented, not rehearsed; remaining mitigations/reviews, distribution clearance and whole-phase acceptance remain outstanding. - **Retained evidence limits:** 9C completes a bounded cleanup/protocol review, not every-library-copy zeroization. Live inline residue, compaction tails, hardware/stack intermediates, global admission starvation and resource/interop measurements remain documented limitations or combined target gates. Any additional hardening must preserve owner lifetimes and bounded recovery. - **Phase 10: OTA trust.** Define independent image-signature verification, trust-anchor provisioning, rotation/revocation, rollback/downgrade and recovery policy without secure boot. Authenticated transport alone is not image-signing policy, and OTA signature checks cannot prevent physical firmware replacement. diff --git a/docs/wolf_security_review.md b/docs/wolf_security_review.md index 8ea6761..4e8493e 100644 --- a/docs/wolf_security_review.md +++ b/docs/wolf_security_review.md @@ -1,28 +1,36 @@ # Phase 9D — wolfSSL / wolfSSH advisory review (partial) -## Implementation addendum — 2026-09-15 +## Implementation addendum — 2026-09-16 **Xtensa small-math mitigation implemented; the broader review and security sign-off remain open.** Root `CMakeLists.txt` defines `CURVE25519_SMALL` and `ED25519_SMALL` before component parsing. `cmake/wolf_crypto_policy.cmake` propagates a forced-include resolved-settings guard PUBLIC from wolfSSL to consumers, including wolfSSH/application code. `cmake/wolf_crypto_policy.h` requires both enabled small implementations, rejects `WOLFSSL_CURVE25519_BLINDING`, and rejects enabling Curve448/Ed448 without review. This follows PR 9275's small-math policy for the enabled Xtensa algorithms, with consistent library/consumer production flags and ABI-sensitive layouts. **Blinding is not enabled with small X25519**: wolfSSL 5.8.2 excludes/rejects this combination; do not force it back on or present the historical blinding observation below as current policy. -**Parser corrections implemented:** `tools/security_overrides.py` extends the existing hash-pinned wolfSSH generated source (still seven overridden files overall). The PR892 subset bounds `DoIgnore`/`GetSkip` and service-string reads, accepts boundary-empty skips, rejects zero-capacity `GetString`, and preserves the old strict service-name length limit. The PR881 subset rejects channel-window addition overflow without changing the window. The PR880 subset changes both ECC/Ed25519 key/signature label comparisons to short-circuit OR. Local framing corrections bound ECC r/s reads to the declared sub-blob and require exact inner/outer consumption; Ed25519 also requires exact outer consumption. Valid framing and crypto/digest/message construction remain unchanged; formerly tolerated malformed labels/trailing bytes reject. Existing password bounds/wiping and async-pending retention remain intact. See the [parser contract and deferred scope](../tests/wolfssh_parser_contract/README.md); these are subsets, not complete PR backports. PR899 has no applied hunks; service semantics, other parsers and standalone ECC key-blob semantics remain open. +**Parser corrections implemented:** `tools/security_overrides.py` extends the existing hash-pinned wolfSSH generated source (now eight C source overrides plus one header overall). The PR892 subset bounds `DoIgnore`/`GetSkip` and service-string reads, accepts boundary-empty skips, rejects zero-capacity `GetString`, and preserves the old strict service-name length limit. The PR881 subset rejects channel-window addition overflow without changing the window. The PR880 subset changes both ECC/Ed25519 key/signature label comparisons to short-circuit OR. Local framing corrections bound ECC r/s reads to the declared sub-blob and require exact inner/outer consumption; Ed25519 also requires exact outer consumption. Valid framing and crypto/digest/message construction remain unchanged; formerly tolerated malformed labels/trailing bytes reject. Existing password bounds/wiping and async-pending retention remain intact. See the [parser contract and deferred scope](../tests/wolfssh_parser_contract/README.md); these are subsets, not complete PR backports. PR899 has no applied hunks; service semantics, other parsers and standalone ECC key-blob semantics remain open. **Crypto validation implemented:** PUBLIC `WOLFSSL_VALIDATE_ECC_IMPORT` and `WOLFSSL_ECDHX_SHARED_NOT_ZERO` enable existing upstream P-256 import validation and X25519 all-zero-result rejection. The guard rejects missing checks and reviewed validator-disabling/hardware-stub configurations. Strict production checks confirm the effective flags without candidate injection. The [key-validation review](ssh_key_validation_review.md) traces the previously missing unauthenticated P-256 KEX point check and low-order X25519 inputs that pass the old precheck. Current raw SSH signature callers supply locally derived full digests; no current short-digest/OID-confusion trigger was found. This does not backport generic PR10131 API hardening or demonstrate an exploit. Import validation adds CPU/allocation cost requiring target measurement. -Supplied parent evidence: `pio run` **PASS**, **94,340 B linked RAM / 1,768,949 B flash**, unchanged RAM / **+1,732 B flash** versus the preceding 1,767,217 B mitigation build. All five [focused commands](security_hardening.md#mitigation-hostbuild-evidence--2026-09-15) passed: strict crypto policy, parser contract (3,124 cases in each of two stack modes plus six rejected guard-removal mutations), auth contract (135 cases), SSH protocol policy, and SDK overrides with actual build-directory registration. Independent review found no blocker in the scoped changes. Parser tests use crypto doubles; crypto tests execute real vendor arithmetic and independently check exact generated-source deltas. Neither establishes an end-to-end SSH exchange. These are supplied results, not build/test reruns by this documentation update or runtime-headroom evidence. +Supplied parent build evidence: `pio run` **PASS**, **94,340 B linked RAM / 1,768,701 B flash**, unchanged RAM / **−248 B flash** versus 1,768,949 B. Final parent results: **all six suites PASS** — ordering `--interop` (8,028 checks, seven rejected mutations, 12 exact-256-KiB sessions with clean channel close/transport EOF), SDK overrides with actual `--build-dir` registration, auth (135 cases), protocol, strict crypto and parser (3,124 cases × two modes). Interop's unsandboxed approval was solely for local AF_UNIX sockets; no remote network or device operation. See [dated evidence and limits](security_hardening.md#ordering-hostbuild-evidence--2026-09-16). These are supplied results, not build/test reruns by this documentation update or runtime-headroom evidence. -**Still pending:** coherent wolfSSH ordering correction (CVE-2025-14942), deferred parser/API/advisory review, whole-phase target validation and [license/source packaging](dependency_licenses.md). No dependency upgrade or device operation was performed. Phase 9 is not complete or production-ready. +**Restricted ordering correction implemented for CVE-2025-14942; not a full upstream backport or advisory/phase sign-off.** Deferred parser/API/advisory review, whole-phase target validation and [license/source packaging](dependency_licenses.md) remain open. No dependency upgrade or device operation was performed. Phase 9 is not complete or production-ready. -### Ordering blocker and actionable next strategy +### Restricted ordering correction and remaining gates -The supplied follow-up reports an attempted coherent PR793/819/840/855/921 backport in temporary work only; **no ordering changes were retained**. Manual patch prerequisites remained unresolved, as did nonblocking `SendNewKeys` returning `WS_WANT_WRITE` and skipping the `SendExtInfo` continuation, and `extInfoSent` semantics across rekey. Parser/crypto passes do not close these state-machine issues. +The earlier temporary attempt is superseded by the [implemented restricted-profile correction and prerequisite disposition](../tools/wolfssh_order/README.md). wolfSSH remains pinned to **1.4.20** with the existing **Curve25519-SHA256 / ECDH-P256** KEX profile; DH/GEX/PQ widening is rejected. Independent SELF/PEER keying bits, exact nonzero expected-message tracking and service/authentication phase gates reject wrong-direction, premature and duplicate traffic. Receive expectations are separate from send-side rekey fences. Pre-peer-KEXINIT in-flight traffic remains legal when only SELF initiated rekey. + +`SendNewKeys` queues one packet under the old sending keys and installs new sending keys; SUCCESS or WANT_WRITE clears SELF, retaining PEER and its expected NEWKEYS. Existing flush paths finish the same buffered packet without resending NEWKEYS. **EXT_INFO is intentionally disabled**, rather than adding a pending extension continuation: no `ext-info-c`, no outgoing EXT_INFO, incoming EXT_INFO rejected, `sendExtInfo`/`extInfoSent` remain zero. There is **no `server-sig-algs` advertisement** now; the KeyAccepted setter does not produce that wire extension. RSA-SHA2 discovery/RSA user-key interoperability is not claimed. + +The executable consolidated edits are [delta.json](../tools/wolfssh_order/delta.json), not all hunks of PR793/819/840/855/921. [provenance.json](../tools/wolfssh_order/provenance.json) records exact archived patch byte hashes, source URLs and full embedded commit IDs; preserve those archives rather than relying on mutable PR responses. Original source/header hashes are pinned, upstream notices retained, and generated wolfSSH files carry the **2026-09-16** ordering modification/provenance notice. There are **eight C overrides plus one generated `wolfssh/internal.h`**. CMake propagates the ABI overlay BEFORE PUBLIC and as a PUBLIC forced include, with a stale-header guard and configure/compiler dependencies. Joined `-include/path` arguments preserve both ordering and crypto forced headers through PlatformIO sorting/deduplication; the adapter regression exercises a real Xtensa consumer and a failing split-option mutation. + +Review found a misplaced EOF guard; it is now corrected in `SendChannelEof` before lookup/serialization/`eofTxd` mutation. The extra `SendChannelExit` guard remains intentional local hardening, not PR793's EOF hunk. Follow-up verification found no scoped blocker. Supplied ordering evidence: **8,028 checks and seven rejected mutations**, including actual EOF/shutdown/exit-status rekey fences. + +The initial host interoperability run failed a close race despite printing `INTEROP PASS` early; that output was not successful completion. The corrected harness waits for peer channel close and transport EOF, uses a local fd-passing proxy, independently owns/reaps the server, and requires both process exits to be zero. Final agent evidence: **36/36 repeated OpenSSH 10.2p1 sessions**, each exact **256 KiB** binary echo, with **ten** completed exchanges for client-rekey cases or **two** for fragmented server-rekey cases, clean exits and no EXT_INFO. See the [test contract, harness diagnosis and limits](../tests/wolfssh_order_contract/README.md). This establishes bounded host interoperability, not general shutdown correctness or device behavior. Official registry queries for wolfSSH **1.5.0** and wolfSSL **5.9.2** returned **404 on 2026-09-15**. Upstream tags exist at wolfSSH commit `8643d7be841184f766374e3b0ed68ced6391543c` and wolfSSL commit `ac01707f552c611fbd135cc723b2682b3e7f80f2`; tag existence is not managed-component availability or ESP compatibility. This is supplied query evidence, not a fresh network check by this documentation update. -Next, evaluate those immutable upstream snapshots in an isolated compatibility branch/worktree with an explicit reviewed packaging/provenance plan, rather than assume a registry version bump works. Alternatively, inventory and review every prerequisite of a coherent source/header backport before applying it. In either approach, first add state-machine regression coverage for partial sends/`WS_WANT_WRITE`, exactly-once EXT_INFO continuation, initial KEX versus rekey and `extInfoSent` lifetime, unexpected/pre-auth messages and valid client flows. Rebase source overrides and version/callback contracts explicitly; preserve password wiping and parser/crypto checks, then rerun focused suites and the firmware build. Only after review and whole-phase target interoperability/resource tests may ordering closure be claimed. +Remaining gates: target cleanup/disconnect during rekey, no-EXT_INFO compatibility with intended clients, initial KEX/rekey under load, and stack/heap/timing measurements during whole-phase validation. The broader advisory/parser review and release packaging remain open. Any future upstream upgrade still needs isolated packaging/compatibility evaluation, coherent source/header review and explicit rebasing of source overrides and version/callback contracts while preserving password/parser/crypto protections. The restricted correction is not a full wolfSSH 1.4.22/1.5.0 backport, dependency upgrade, or phase sign-off. ## Historical pre-mitigation research baseline — 2026-09-15 -**The remainder retains the original pre-mitigation research evidence. “Current” macros, generated hashes, priorities and “not applied/tested” statements below describe that historical snapshot, not today's implementation. The addendum and linked key-validation review supersede the scoped small-math, parser and ECC/X25519 status claims. Ordering and explicitly deferred findings remain open.** +**The remainder retains the original pre-mitigation research evidence. “Current” macros, generated hashes, priorities and “not applied/tested” statements below describe that historical snapshot, not today's implementation. The addendum and linked key-validation review supersede the scoped small-math, parser, ECC/X25519 and ordering status claims. Restricted ordering is implemented; its target/compatibility gates and explicitly deferred findings remain open.** Review date: **2026-09-15**. Read-only external research and local applicability inspection; stopped at the user's requested handoff. **Not a completed security review or release clearance.** Only this new report was written. No dependencies, sources, generated inputs, or parent documents were changed; no build, hardware test, network exploit, or public PoC was executed. diff --git a/tests/sdk_security_overrides/run.py b/tests/sdk_security_overrides/run.py index 72f2cca..b9a3f58 100644 --- a/tests/sdk_security_overrides/run.py +++ b/tests/sdk_security_overrides/run.py @@ -27,6 +27,13 @@ sdk = importlib.util.module_from_spec(SPEC) sys.modules[SPEC.name] = sdk SPEC.loader.exec_module(sdk) TLS_ENTRY = next(e for e in sdk.ENTRIES if e.name == "esp_tls_mbedtls") +SOURCE_ENTRIES = tuple(e for e in sdk.ENTRIES if not e.header) + + +def generated_path(binary, entry): + base = binary / "security_overrides" + return (base / "wolfssh_include/wolfssh" / Path(entry.source).name if entry.header + else base / entry.name / Path(entry.source).name) def source_path(entry, idf, project=ROOT): @@ -91,17 +98,32 @@ def generator_tests(idf, work): assert before == {p: (p.read_bytes(), p.stat().st_mtime_ns) for p in before} for entry in sdk.ENTRIES: original = source_path(entry, idf).read_bytes() - derived = (binary / "security_overrides" / entry.name / Path(entry.source).name).read_bytes() + derived = generated_path(binary, entry).read_bytes() notice = sdk.MODIFICATION_NOTICE.encode() assert derived.startswith(notice) assert derived.count(notice) == 1 assert b"Modified by the ESP32_serial_swiss_army_knife project on 2026-09-15" in notice - assert derived[len(notice):].startswith(original[:original.index(b"*/") + 2]) + assert original[:original.index(b"*/") + 2] in derived[:2500] + if entry.component == "wolfssl__wolfssh": + assert b"Ordering profile modified 2026-09-16" in derived[:1000] assert derived != original expect_error(lambda: sdk.render_entry(replace(sdk.ENTRIES[0], target="mbedtls"), {"idf": idf, "project": ROOT}), "invalid nested target") expect_error(lambda: sdk.render_entry(replace(sdk.ENTRIES[1], target="unknown"), {"idf": idf, "project": ROOT}), "invalid nested target") + expect_error(lambda: sdk.render_entry(replace(sdk.ENTRIES[0], header=True), + {"idf": idf, "project": ROOT}), "unaudited header overlay") + project_copy = work / "header_mismatch" + for entry in sdk.ENTRIES: + if entry.root == "project": + copied = project_copy / entry.source + copied.parent.mkdir(parents=True, exist_ok=True) + shutil.copyfile(ROOT / entry.source, copied) + header = next(e for e in sdk.ENTRIES if e.header) + (project_copy / header.source).write_bytes(b"changed header") + header_failed = work / "header_failed" + expect_error(lambda: sdk.generate(idf, project_copy, header_failed), "SHA256 mismatch") + assert not header_failed.exists(), "header drift must reject the entire source/ABI plan" expect_error(lambda: sdk.apply_edits("x", (sdk.Edit("missing", "z"),)), "got 0") expect_error(lambda: sdk.apply_edits("xx", (sdk.Edit("x", "z"),)), "got 2") expect_error(lambda: sdk.generate(idf, ROOT, binary, ()), "absent") @@ -136,7 +158,7 @@ def generator_tests(idf, work): def extracted_tests(idf, binary, work): - texts = {e.name: (binary / "security_overrides" / e.name / Path(e.source).name).read_text() for e in sdk.ENTRIES} + texts = {e.name: generated_path(binary, e).read_text() for e in sdk.ENTRIES} aux = {} for rel, expected in AUXILIARY.items(): raw = (idf / rel).read_bytes() @@ -224,6 +246,8 @@ def cmake_fixture_tests(idf, work): copied = fixture / e.source copied.parent.mkdir(parents=True, exist_ok=True) shutil.copyfile(source_path(e, idf), copied) + if e.header: + continue target = e.target or f"test_{e.component}" owner_lines = nested_lines if e.target else lines registered_source = Path(e.source).name if e.target else source_path(e, idf, fixture) @@ -245,11 +269,16 @@ def cmake_fixture_tests(idf, work): 'if(TEST_AMBIGUOUS)', f'set_property(TARGET test_{sdk.ENTRIES[0].component} APPEND PROPERTY SOURCES "{source_path(sdk.ENTRIES[0], idf, fixture)}")', 'endif()', 'if(TEST_TARGET_MISSING)', 'function(idf_component_get_property out component property)', 'set(${out} nonexistent PARENT_SCOPE)', 'endfunction()', 'endif()'] - for e in sdk.ENTRIES: + for e in SOURCE_ENTRIES: directory = f' DIRECTORY "{nested_dir}"' if e.target else '' lines += [f'set_source_files_properties("{source_path(e, idf, fixture)}"{directory} PROPERTIES COMPILE_FLAGS "-DSOURCE_FLAG" COMPILE_DEFINITIONS "SOURCE_DEFINE" COMPILE_OPTIONS "-fno-common")'] - lines += [f'include("{ROOT / "cmake/security_overrides.cmake"}")'] - for e in sdk.ENTRIES: + (fixture / 'consumer.c').write_text('int consumer(void) { return 0; }\n') + lines += ['add_library(direct_consumer STATIC consumer.c)', + 'target_link_libraries(direct_consumer PUBLIC test_wolfssl__wolfssh)', + 'add_library(transitive_consumer STATIC consumer.c)', + 'target_link_libraries(transitive_consumer PRIVATE direct_consumer)', + f'include("{ROOT / "cmake/security_overrides.cmake"}")'] + for e in SOURCE_ENTRIES: target = e.target or f"test_{e.component}" directory = f' DIRECTORY "{nested_dir}"' if e.target else '' lines += [f'file(GENERATE OUTPUT "${{CMAKE_BINARY_DIR}}/{e.name}.sources" CONTENT "$")', @@ -260,24 +289,33 @@ def cmake_fixture_tests(idf, work): (fixture / "CMakeLists.txt").write_text("\n".join(lines) + "\n") build = work / "cmake_good" run(["cmake", "-G", "Ninja", "-S", fixture, "-B", build]) - for e in sdk.ENTRIES: + for e in SOURCE_ENTRIES: source = (build / (e.name + ".sources")).read_text() assert source.split(';').count(str(build / "security_overrides" / e.name / Path(e.source).name)) == 1 assert str(source_path(e, idf, fixture)) not in source build_registration(build, idf, fixture) commands = json.loads((build / "compile_commands.json").read_text()) - for e in sdk.ENTRIES: - generated = str(build / "security_overrides" / e.name / Path(e.source).name) + for e in SOURCE_ENTRIES: + generated = str(generated_path(build, e)) matches = [c for c in commands if c["file"] == generated] assert len(matches) == 1, (e.name, matches) for option in ("-DSOURCE_FLAG", "-DSOURCE_DEFINE", "-fno-common", str(source_path(e, idf, fixture).parent)): assert option in matches[0]["command"], (e.name, option, matches) + header = next(e for e in sdk.ENTRIES if e.header) + overlay = str(generated_path(build, header)) + consumers = [c for c in commands if c['file'].endswith('/consumer.c') or + '/wolfssh_internal/' in c['file'] or '/wolfssh_ssh/' in c['file']] + assert len(consumers) == 4 + for command in consumers: + assert '-include' + overlay in command['command'] + assert str(build / 'security_overrides/wolfssh_include') in command['command'] + print('PUBLIC forced header/overlay reaches library, direct and transitive consumers PASS') ninja = (build / "build.ninja").read_text() - for path in [ROOT / "tools/security_overrides.py", idf / "components/esp_common/include/esp_idf_version.h"] + [source_path(e, idf, fixture) for e in sdk.ENTRIES]: + for path in [ROOT / "tools/wolfssh_order/delta.json", ROOT / "tools/security_overrides.py", idf / "components/esp_common/include/esp_idf_version.h"] + [source_path(e, idf, fixture) for e in sdk.ENTRIES]: assert str(path) in next(line for line in ninja.splitlines() if ": RERUN_CMAKE" in line), path for flag, phrase in (("TEST_MISSING", "found 0"), ("TEST_AMBIGUOUS", "found 2"), - ("TEST_TARGET_MISSING", "missing component target"), + ("TEST_TARGET_MISSING", "missing wolfSSH overlay target"), ("TEST_NESTED_MISSING", "missing nested target"), ("TEST_NESTED_OWNER", "unexpected nested target owner"), ("TEST_mbedtls_MISSING_SOURCE", "found 0"), @@ -345,7 +383,11 @@ def build_registration(build, idf, project=ROOT): ninja = (build / "build.ninja").read_text() compile_lines = [line for line in ninja.splitlines() if ": C_COMPILER" in line] for e in sdk.ENTRIES: - generated = build / "security_overrides" / e.name / Path(e.source).name + generated = generated_path(build, e) + if e.header: + assert not any(str(generated) in line for line in compile_lines) + assert generated.read_bytes() == sdk.render_entry(e, {"idf": idf, "project": project})[1] + continue matches = [line for line in compile_lines if str(generated) in line] assert len(matches) == 1, (e.name, matches) assert not any(str(source_path(e, idf, project)) in line for line in compile_lines), e.name diff --git a/tests/ssh_protocol_policy/run.py b/tests/ssh_protocol_policy/run.py index 894cbff..70a0fc4 100644 --- a/tests/ssh_protocol_policy/run.py +++ b/tests/ssh_protocol_policy/run.py @@ -117,6 +117,28 @@ def enum_containing(source, token): return matches[0] + "\n" +def reviewed_order_function(name, original): + """Independent, exact allowlist; do not accept an arbitrary generator delta.""" + if name == 'SendExtInfo': + return ('int SendExtInfo(WOLFSSH* ssh)\n{\n' + ' WOLFSSH_UNUSED(ssh);\n return WS_NOT_COMPILED;\n}\n') + if name == 'SendKexInit': + edits = ( + (' ssh->isKeying = 1;', + ' /* Set self is keying flag since we started sending the KEX init msg */\n' + ' ssh->isKeying |= WOLFSSH_SELF_IS_KEYING;'), + (' if (ssh->ctx->side == WOLFSSH_ENDPOINT_CLIENT) {\n' + ' kexAlgoNamesPlus = ",ext-info-c";\n' + ' kexAlgoNamesPlusSz = (word32)WSTRLEN(kexAlgoNamesPlus);\n }\n\n', ''), + (' if (ret == WS_SUCCESS)\n ret = wolfSSH_SendPacket(ssh);', + ' if (ret == WS_SUCCESS) {\n ret = wolfSSH_SendPacket(ssh);\n }'), + ) + for old, new in edits: + assert original.count(old) == 1, name + original = original.replace(old, new) + return original + + def main(): parser = argparse.ArgumentParser(description=__doc__) databases = sorted((ROOT / ".pio/build").glob("*/compile_commands.json")) @@ -182,15 +204,23 @@ def main(): functions = ("NameToId", "IdToName", "AlgoListSz", "CopyNameList", "CopyNameListPlus", "BuildNameList", "SendKexInit", "SendExtInfo") - actual = "\n".join(extract(sources["ssh.c"], "wolfSSH_CTX_SetAlgoList" + field) - for field in FIELDS) + ssh_entry = next(e for e in ENTRIES if e.name == 'wolfssh_ssh') + _, generated_ssh = render_entry(ssh_entry, {'project': ROOT}) + actual = '' + for field in FIELDS: + name = 'wolfSSH_CTX_SetAlgoList' + field + body = extract(generated_ssh.decode(), name) + assert body == extract(sources['ssh.c'], name), name + actual += body for name in functions: - if extract(internal, name) != extract(sources["internal.c"], name): + if extract(internal, name) != reviewed_order_function(name, extract(sources["internal.c"], name)): raise RuntimeError(f"Override changed reviewed protocol function: {name}") actual += "\n".join(extract(internal, name) for name in functions) # Preserve actual conditional enum values and feature-filtered name table. types = "\n".join(enum_containing(resolved, token) for token in ("ID_NONE", "TYPE_KEX", "MSGID_KEXINIT", "WOLFSSH_ENDPOINT_SERVER")) + assert macros['WOLFSSH_SELF_IS_KEYING'] == '0x02' + types += '#define WOLFSSH_SELF_IS_KEYING 0x02\n' types += "typedef struct { byte id; byte type; const char *name; } NameIdPair;\n" + mapping assignments = [] for field in FIELDS: diff --git a/tests/ssh_protocol_policy/vendor.c b/tests/ssh_protocol_policy/vendor.c index 00857d5..251a659 100644 --- a/tests/ssh_protocol_policy/vendor.c +++ b/tests/ssh_protocol_policy/vendor.c @@ -7,6 +7,7 @@ #include "resolved.h" #define WLOG(...) ((void)0) +#define WOLFSSH_UNUSED(x) ((void)(x)) #define INLINE inline #define WMEMCPY memcpy #define WSTRLEN strlen @@ -18,9 +19,7 @@ #define LENGTH_SZ 4U #define BOOLEAN_SZ 1U #define COOKIE_SZ 16U -#define WS_EXTINFO_EXTENSION_COUNT 1 static const char cannedNoneNames[] = "none"; -static const char serverSigAlgsName[] = "server-sig-algs"; typedef struct { byte *kexInit; word32 kexInitSz; } HandshakeInfo; typedef struct { @@ -206,14 +205,13 @@ int main(void) assert(SendKexInit(ssh) == WS_SUCCESS); check_kex(ssh); assert(sends == 2 && allocations == 2 && frees == 1); - assert(SendExtInfo(ssh) == WS_SUCCESS); - size_t offset = 1, length = ssh->outputBuffer.length - 8U; - const byte *p = packet + 8; - assert(p[0] == MSGID_EXT_INFO); - assert(take_u32(p, length, &offset) == 1); - expect_name(p, length, &offset, "server-sig-algs"); - expect_name(p, length, &offset, "ssh-ed25519,ecdsa-sha2-nistp256"); - assert(offset == length); + assert(ssh->isKeying == WOLFSSH_SELF_IS_KEYING); + assert(SendExtInfo(ssh) == WS_NOT_COMPILED); + assert(sends == 2); + ctx.side = WOLFSSH_ENDPOINT_CLIENT; + assert(SendKexInit(ssh) == WS_SUCCESS); + check_kex(ssh); /* Client must not append ext-info-c either. */ + ctx.side = WOLFSSH_ENDPOINT_SERVER; /* No key and injected packet/allocation failures must not send a fallback. */ unsigned before = sends; ctx.privateKeyCount = 0; @@ -234,6 +232,6 @@ int main(void) assert(purges == old_purges); check_kex(ssh); bounded_free(handshake.kexInit); - puts("PASS: resolved vendor name/ID/type map, actual setters, initial/rekey KEXINIT both directions, server-sig-algs, bounded failure paths"); + puts("PASS: resolved vendor name/ID/type map, actual setters, initial/rekey KEXINIT both directions, no EXT_INFO negotiation, bounded failure paths"); return 0; } diff --git a/tests/wolf_crypto_policy/run.py b/tests/wolf_crypto_policy/run.py index b5251ec..4939314 100644 --- a/tests/wolf_crypto_policy/run.py +++ b/tests/wolf_crypto_policy/run.py @@ -207,7 +207,7 @@ def profiles(database, candidate): 'wolfcrypt/src/curve25519.c', 'wolfcrypt/src/ed25519.c', 'wolfcrypt/src/fe_operations.c', 'wolfcrypt/src/ge_operations.c', 'wolfcrypt/src/fe_low_mem.c', 'wolfcrypt/src/ge_low_mem.c', - 'wolfssl__wolfssh/src/ssh.c', + 'security_overrides/wolfssh_ssh/ssh.c', 'security_overrides/wolfssh_internal/internal.c', 'src/ssh_transport.c', 'src/ssh_security.c') for suffix in suffixes: diff --git a/tests/wolfssh_order_contract/README.md b/tests/wolfssh_order_contract/README.md new file mode 100644 index 0000000..2d391c7 --- /dev/null +++ b/tests/wolfssh_order_contract/README.md @@ -0,0 +1,182 @@ +# Generated wolfSSH ordering contract + +Implementation/provenance and restricted-profile rationale: +[`tools/wolfssh_order/README.md`](../../tools/wolfssh_order/README.md). + +```sh +python3 tests/wolfssh_order_contract/run.py +python3 tests/wolfssh_order_contract/run.py --interop --target-contracts +python3 tests/wolfssh_order_contract/run.py --interop --interop-repeat 3 +python3 tests/wolfssh_parser_contract/run.py +python3 tests/wolfssh_auth_contract/run.py --host-only +python3 tests/sdk_security_overrides/run.py +python3 tests/wolf_crypto_policy/run.py --host-only +``` + +Requires the already-installed pinned managed components, a host C compiler, +and (for `--interop`) OpenSSH `ssh`/`ssh-keygen` with `ProxyUseFdpass`, OpenSSL, +and POSIX Unix sockets with `SCM_RIGHTS` descriptor passing. The +optional `--target-contracts` also requires the existing ESP-IDF compiler and +`.pio/build/esp32-s3-devkitc-1-n16r8/compile_commands.json`. It **does not run +PlatformIO**. Everything generated by this suite is in a temporary directory; +it never writes managed sources, persistent keys or the production build tree. +No downloads or IP network sockets: a short-lived ProxyCommand passes a temporary +local Unix socket to OpenSSH. Python independently owns/reaps the server, whose +stdio uses the accepted socket. User/global SSH configuration and agents are disabled. +Sandboxes that prohibit even AF_UNIX sockets require permission for `--interop`. +All keys +and the fixed test password are disposable fixtures, not production credentials. + +## Follow-up fixes and validation — 2026-09-16 + +`python3 tests/wolfssh_order_contract/pio_adapter.py` executes the installed +PlatformIO `get_app_flags` function and SCons ParseFlags/AppendUnique on the +configured project flags. Both forced headers must remain joined `-include/path` +arguments. A real Xtensa `-c/-o` consumer compiles; a split-option mutation must +reproduce the multiple-input failure. Also available as `run.py --pio-adapter`. + +Real EOF, shutdown and exit-status tests now cover SELF, PEER and SELF|PEER, +empty/pending output, stale WANT_WRITE and repeated calls: no bytes, callbacks, +sequence changes, expectation consumption, `eofTxd` or `closeTxd` mutation. +EOF succeeds exactly once after keying clears. The misplaced PR793 EOF guard is +corrected; the additional exit-status guard is intentionally retained. Removing +the EOF guard is the seventh required failing mutation. Latest run: 8,028 checks. + +Authorized firmware build PASS: `pio run`, 21.31 s, **94,340 B RAM / 1,768,701 B +flash**. Strict SDK registration, auth, protocol and crypto suites PASS against +that build. This supersedes the initial no-build/stale-artifact status below; +no device validation occurred. The 12-session OpenSSH matrix was not rerun in +this narrowly scoped follow-up; its previous evidence remains historical. + +## What executes + +The generator verifies original source hashes and applies the real checked-in +edits. The test includes the **entire generated `internal.c`**, links generated +`ssh.c`, unmodified pinned IO/log/port sources, and real wolfCrypt code. Static +functions are directly visible to the test; they are not rewritten copies or +models. Every test translation unit uses the generated ABI header. The original +include directory is deliberately searched first to exercise the forced overlay. +An original-header-before-overlay compile must fail explicitly. + +The host crypto settings enable portable small X25519/Ed25519, TFM P-256, +import/shared-secret validation and AES-GCM. They are **host settings**, not an +assertion that every ESP compile option is identical. UBSan trap instrumentation +is enabled; the host's standalone UBSan runtime is unavailable. `WOLFSSL_USE_ALIGN` +selects bytewise encoding so x86 unaligned fast-path stores do not mask protocol +tests with alignment traps. Production crypto policy is not changed. + +Coverage: + +- Every byte-sized message ID at initial state for both roles; every ID during + all combinations of self/peer keying; explicit service/auth phase boundaries. +- Actual `DoPacket` rejects malformed pre-auth auth/channel/extension payloads + **before dispatch**, for both roles, without consuming the input. +- Missing, wrong, duplicate and prematurely received KEX/NEWKEYS messages; + benign transport notifications do not consume an expectation. +- Wrong optimistic INIT guess preserves the real INIT expectation; duplicate + `DoKexInit` fails before parsing another exchange. +- Actual NEWKEYS framing, queued bytes, sequence counter and key installation: + zero-through-complete one-byte write quotas, repeated output flush, no duplicate + NEWKEYS, independent self/peer bits and exactly-once handshake disposal. + Fatal IO and real invalid-AES-key errors retain the required keying state. +- Generated client/server complete password-authenticated handshakes with both + KEX algorithms and AES256-GCM, followed by server-, client- and simultaneously + initiated rekeys. IO fragments to 11/13 bytes and injects WANT_WRITE regularly. +- Six deliberately bad generated-source mutations must fail: pre-auth injection, + wrong expected KEX, unnegotiated EXT_INFO, clearing both keying bits, accepting + peer NEWKEYS before local NEWKEYS, and removing PR921's server expectation. +- Archived upstream patch hashes/commit IDs, source/header pins and unchanged + logging ABI are independently checked. + +## OpenSSH interoperability (`--interop`) + +Twelve real sessions cover: + +- X25519 and P-256 KEX, P-256 host identity and AES128-GCM. +- Ed25519 public-key, P-256 public-key and password authentication. +- Client rekey every 32 KiB, or a server-initiated rekey with 31/37-byte fragmented + IO and forced WANT_WRITE every third send callback. +- Exact 256 KiB binary echo per session, successful channel close/status, + at least two completed exchanges and **no EXT_INFO/server-sig-algs received**. + +Verified with OpenSSH **10.2p1 / OpenSSL 3.5.8** on 2026-09-16. The unfragmented +client-rekey fixtures completed ten exchanges each; fragmented server-rekey +fixtures completed two and forced roughly 3,600 write stalls each. These are +host interoperability results, not device measurements or universal-client +claims. Test-only authentication authorizes a generated key blob or a synthetic +password; public-key signature verification is still performed by real wolfSSH. + +### Closure-race diagnosis and regression — 2026-09-16 + +The original harness failure was reproduced: exact echo and `INTEROP PASS` were +followed by OpenSSH `Broken pipe` while sending channel close. The harness called +`wolfSSH_shutdown()` once, accepted WANT_READ as retryable, drained only output, +and exited without receiving the peer's close. Flushing is not shutdown completion. +A second lifecycle issue is that OpenSSH terminates its ProxyCommand on exit; +keeping the server itself as that proxy cannot reliably prove server completion. + +The harness now queues shutdown once, flushes output, pumps the real worker until +`WS_CHANNEL_CLOSED` and channel removal, then keeps receiving through transport +EOF. It accepts the expected socket-close error only with an actual zero-length +transport read, the library closed flag, and no reset. Python owns the server +independently via a local fd-passing proxy; no sleep-based grace period or return +code waiver is used. The existing 100-us polling backoff is not a close deadline. +Shutdown remains unfragmented, as before; this does not expand nonblocking-shutdown +coverage beyond the separate contract tests. + +Each session requires **both process exit codes zero**, byte-for-byte 262,144-byte +echo, a single complete server evidence record, at least two completed exchanges, +the expected signed/password authentication counts, stalls in fragmented cases, +peer channel-close completion and transport EOF. OpenSSH must independently report +receipt of exit-status and channel close and `Exit status 0`; EXT_INFO remains +forbidden. Early server exit, missing evidence, nonzero client/server status, +truncated/corrupted echo, and missing rekey evidence all fail. + +`--interop-repeat N` repeats the full 12-session matrix (1–20, default 1), compiling +the contracts once and generating new disposable keys per matrix. Bounds: 10 s +proxy connection, 30 s server alarm, 45 s client communication, 5 s server reap; +failed/timed-out processes are killed and reaped. No production code changes are +needed for the reproduced harness race. Final validation on OpenSSH 10.2p1: +`python3 tests/wolfssh_order_contract/run.py --interop --interop-repeat 3` passed +**36/36 sessions**, plus 8,028 contract checks and seven rejected mutations. +Client-rekey cases completed ten exchanges and server-rekey cases two; each +session had exact echo and both clean process exits. An earlier revision of the +fix also passed 36 sessions before the final close/EOF-error assertions were added. +These results do not establish general library shutdown correctness or hardware +behavior. + +## Candidate target contracts (`--target-contracts`) + +Reads, but does not modify, existing compiler commands. Preserves their flags +and crypto definitions while substituting freshly generated SSH sources and the +PUBLIC-equivalent header overlay in a **temporary candidate database**. Runs: + +- Xtensa syntax checks for generated `internal.c`/`ssh.c` and SSH application + consumers. +- `ssh_protocol_policy`, including independently specified exact changes to + KEXINIT and disabled EXT_INFO serialization (not an arbitrary delta allowlist). +- `wolf_crypto_policy`, including real vendor vectors, independently checked + protected crypto/parser function bodies and resolved compiler flags. +- `wolfssh_auth_contract`, including password bounds/wiping/async contracts. + +Those existing suites label some checks “production compiler/profile”; in this +mode they use **production-derived flags with candidate source/header paths**. +The wrapper explicitly labels the run candidate-only. This does not establish +actual firmware build registration, linking, flashing, resource reserve or +hardware behavior. Their ordinary strict commands correctly reject the old +production generated source until a firmware reconfigure is performed. + +The SDK override suite separately executes the actual CMake module against a +mock IDF target graph: source ownership/properties, header hash-drift atomic +failure, PUBLIC overlay/forced include propagation to direct and transitive +consumers, configure dependencies and rejection cases. This is real CMake wiring +evidence, not a full ESP-IDF firmware build. + +## Limits + +No whole-upstream-fix claim. EXT_INFO and keyboard-interactive are excluded; KEX +is intentionally limited to the project algorithms. No RSA-SHA2 discovery, +DH/GEX/PQ, arbitrary-client, hardware, timing, memory-headroom, or complete SSH +parser audit claim. Existing parser/password protections remain covered by their +own suites. Firmware reconfigure/build and whole-phase device validation remain +follow-up work, deliberately not executed in this task. diff --git a/tests/wolfssh_order_contract/contract.c b/tests/wolfssh_order_contract/contract.c new file mode 100644 index 0000000..930ea18 --- /dev/null +++ b/tests/wolfssh_order_contract/contract.c @@ -0,0 +1,288 @@ +/* SPDX-License-Identifier: GPL-3.0-only */ +#include +#include +#include +#include +#include +#include +/* The entire freshly generated translation unit, not a model of its gates. */ +#include "internal.c" + +static unsigned checks; +#define CHECK(x) do { ++checks; if (!(x)) { \ + fprintf(stderr, "FAIL line %d: %s\n", __LINE__, #x); abort(); } } while (0) + +typedef struct { + byte bytes[32768]; + word32 size; + int quota, chunk, calls, fatal; +} Sink; + +static int send_test(WOLFSSH* ssh, void* buf, word32 size, void* context) +{ + Sink* sink = context; + (void)ssh; + ++sink->calls; + if (sink->fatal) return WS_CBIO_ERR_GENERAL; + if (sink->quota == 0) return WS_CBIO_ERR_WANT_WRITE; + if (sink->quota > 0) --sink->quota; + if (size > (word32)sink->chunk) size = sink->chunk; + CHECK(size <= sizeof(sink->bytes) - sink->size); + memcpy(sink->bytes + sink->size, buf, size); + sink->size += size; + return (int)size; +} + +static void policy(WOLFSSH_CTX* ctx) +{ + CHECK(wolfSSH_CTX_SetAlgoListKex(ctx, "curve25519-sha256,ecdh-sha2-nistp256") == 0); + CHECK(wolfSSH_CTX_SetAlgoListKey(ctx, "ecdsa-sha2-nistp256") == 0); + CHECK(wolfSSH_CTX_SetAlgoListCipher(ctx, "aes128-gcm@openssh.com,aes256-gcm@openssh.com") == 0); + CHECK(wolfSSH_CTX_SetAlgoListMac(ctx, "hmac-sha2-256") == 0); + CHECK(wolfSSH_CTX_SetAlgoListKeyAccepted(ctx, "ssh-ed25519,ecdsa-sha2-nistp256") == 0); +} + +static void gates(WOLFSSH* ssh) +{ + byte consumed; + int side, state, msg; + for (side = WOLFSSH_ENDPOINT_SERVER; side <= WOLFSSH_ENDPOINT_CLIENT; ++side) { + ssh->ctx->side = side; + ssh->acceptState = ACCEPT_BEGIN; + ssh->connectState = CONNECT_BEGIN; + for (msg = 0; msg < 256; ++msg) { + ssh->isKeying = 0; + int expect = (msg >= MSGID_DISCONNECT && msg <= MSGID_DEBUG) || msg == MSGID_KEXINIT; + CHECK(IsMessageAllowed(ssh, msg, WS_MSG_RECV) == expect); + } + for (state = 1; state <= 3; ++state) { + ssh->isKeying = state; + ssh->acceptState = ACCEPT_SERVER_USERAUTH_SENT; + ssh->connectState = CONNECT_SERVER_USERAUTH_ACCEPT_DONE; + for (msg = 0; msg < 256; ++msg) { + ssh->handshake->expectMsgId = MSGID_NEWKEYS; + int result = IsMessageAllowed(ssh, msg, WS_MSG_RECV); + if (state & WOLFSSH_PEER_IS_KEYING) { + CHECK(result == ((msg >= 1 && msg <= 4) || msg == MSGID_NEWKEYS)); + CHECK(ssh->handshake->expectMsgId == (msg == MSGID_NEWKEYS ? MSGID_NONE : MSGID_NEWKEYS)); + } + CHECK(!IsMessageAllowed(ssh, msg, WS_MSG_SEND)); + } + } + ssh->isKeying = WOLFSSH_PEER_IS_KEYING; + ssh->handshake->expectMsgId = MSGID_NONE; + CHECK(!IsMessageAllowed(ssh, MSGID_NEWKEYS, WS_MSG_RECV)); + CHECK(!IsMessageAllowed(ssh, MSGID_KEXDH_INIT, WS_MSG_RECV)); + ssh->handshake->expectMsgId = MSGID_NEWKEYS; + CHECK(!IsMessageAllowed(ssh, MSGID_KEXDH_REPLY, WS_MSG_RECV)); + CHECK(ssh->handshake->expectMsgId == MSGID_NEWKEYS); + CHECK(IsMessageAllowed(ssh, MSGID_IGNORE, WS_MSG_RECV)); + CHECK(ssh->handshake->expectMsgId == MSGID_NEWKEYS); + } + ssh->ctx->side = WOLFSSH_ENDPOINT_SERVER; + ssh->isKeying = 0; + ssh->acceptState = ACCEPT_KEYED; + CHECK(IsMessageAllowed(ssh, MSGID_SERVICE_REQUEST, WS_MSG_RECV)); + CHECK(!IsMessageAllowed(ssh, MSGID_USERAUTH_REQUEST, WS_MSG_RECV)); + ssh->acceptState = ACCEPT_SERVER_USERAUTH_ACCEPT_SENT; + CHECK(IsMessageAllowed(ssh, MSGID_USERAUTH_REQUEST, WS_MSG_RECV)); + CHECK(!IsMessageAllowed(ssh, MSGID_SERVICE_REQUEST, WS_MSG_RECV)); + CHECK(!IsMessageAllowed(ssh, MSGID_USERAUTH_SUCCESS, WS_MSG_RECV)); + CHECK(!IsMessageAllowed(ssh, MSGID_CHANNEL_OPEN, WS_MSG_RECV)); + ssh->acceptState = ACCEPT_SERVER_USERAUTH_SENT; + CHECK(IsMessageAllowed(ssh, MSGID_CHANNEL_OPEN, WS_MSG_RECV)); + CHECK(!IsMessageAllowed(ssh, MSGID_USERAUTH_REQUEST, WS_MSG_RECV)); + CHECK(!IsMessageAllowed(ssh, MSGID_EXT_INFO, WS_MSG_RECV)); + CHECK(SendExtInfo(ssh) == WS_NOT_COMPILED); + + ssh->ctx->side = WOLFSSH_ENDPOINT_CLIENT; + ssh->connectState = CONNECT_KEYED; + CHECK(!IsMessageAllowed(ssh, MSGID_USERAUTH_FAILURE, WS_MSG_RECV)); + CHECK(!IsMessageAllowed(ssh, MSGID_USERAUTH_SUCCESS, WS_MSG_RECV)); + CHECK(!IsMessageAllowed(ssh, MSGID_CHANNEL_OPEN_CONF, WS_MSG_RECV)); + ssh->connectState = CONNECT_CLIENT_USERAUTH_SENT; + CHECK(IsMessageAllowed(ssh, MSGID_USERAUTH_FAILURE, WS_MSG_RECV)); + CHECK(IsMessageAllowed(ssh, MSGID_USERAUTH_SUCCESS, WS_MSG_RECV)); + ssh->connectState = CONNECT_SERVER_USERAUTH_ACCEPT_DONE; + CHECK(!IsMessageAllowed(ssh, MSGID_USERAUTH_FAILURE, WS_MSG_RECV)); + CHECK(IsMessageAllowed(ssh, MSGID_CHANNEL_OPEN_CONF, WS_MSG_RECV)); + + /* A wrong first_kex_packet_follows guess does not consume the real INIT. */ + ssh->ctx->side = WOLFSSH_ENDPOINT_SERVER; + ssh->isKeying = WOLFSSH_PEER_IS_KEYING; + ssh->handshake->kexPacketFollows = 1; + ssh->handshake->kexIdGuess = ID_UNKNOWN; + ssh->handshake->kexId = ID_CURVE25519_SHA256; + ssh->handshake->expectMsgId = MSGID_KEXDH_INIT; + CHECK(IsMessageAllowed(ssh, MSGID_KEXDH_INIT, WS_MSG_RECV)); + word32 idx = 0; + byte guessed[1] = {0}; + CHECK(DoKexDhInit(ssh, guessed, sizeof(guessed), &idx) == WS_SUCCESS); + CHECK(idx == 1 && ssh->handshake->expectMsgId == MSGID_KEXDH_INIT); + CHECK(DoKexInit(ssh, guessed, sizeof(guessed), &idx) == WS_INVALID_STATE_E); + + /* Real dispatch must fail before parsing malformed auth/channel payloads. */ + for (side = WOLFSSH_ENDPOINT_SERVER; side <= WOLFSSH_ENDPOINT_CLIENT; ++side) { + ssh->ctx->side = side; + ssh->connectState = CONNECT_BEGIN; + for (msg = 0; msg < 256; ++msg) { + if (msg == MSGID_KEXINIT || (msg >= 1 && msg <= 4)) continue; + ssh->acceptState = ACCEPT_BEGIN; + ssh->isKeying = 0; + ssh->inputBuffer.idx = 0; + ssh->curSz = 8; + memset(ssh->inputBuffer.buffer, 0, 12); + ssh->inputBuffer.buffer[4] = 6; + ssh->inputBuffer.buffer[5] = msg; + CHECK(DoPacket(ssh, &consumed) == WS_MSGID_NOT_ALLOWED_E); + CHECK(consumed == 0); + } + } + ssh->ctx->side = WOLFSSH_ENDPOINT_SERVER; +} + +static void newkeys(WOLFSSH_CTX* ctx) +{ + int quota; + for (quota = 0; quota <= 16; ++quota) { + WOLFSSH* ssh = wolfSSH_new(ctx); + Sink sink = {.quota = quota, .chunk = 1}; + CHECK(ssh != NULL); + CHECK(ssh->isKeying == 0); + wolfSSH_SetIOWriteCtx(ssh, &sink); + ssh->isKeying = WOLFSSH_SELF_IS_KEYING | WOLFSSH_PEER_IS_KEYING; + ssh->handshake->expectMsgId = MSGID_NEWKEYS; + ssh->handshake->encryptId = ID_AES128_GCM; + ssh->handshake->aeadMode = 1; + ssh->handshake->blockSz = 16; + ssh->handshake->keys.encKeySz = 16; + ssh->handshake->peerKeys.encKeySz = 16; + ssh->handshake->keys.ivSz = ssh->handshake->peerKeys.ivSz = 12; + CHECK(DoNewKeys(ssh, NULL, 0, NULL) == WS_INVALID_STATE_E); + int ret = SendNewKeys(ssh); + CHECK(ret == WS_SUCCESS || ret == WS_WANT_WRITE); + CHECK(ssh->isKeying == WOLFSSH_PEER_IS_KEYING); + CHECK(ssh->handshake->expectMsgId == MSGID_NEWKEYS); + word32 seq = ssh->seq; + sink.quota = -1; + CHECK(wolfSSH_SendPacket(ssh) == WS_SUCCESS); + CHECK(ssh->seq == seq); + CHECK(sink.bytes[5] == MSGID_NEWKEYS); + CHECK(sink.size == 16); + CHECK(ssh->outputBuffer.length == 0); + CHECK(IsMessageAllowed(ssh, MSGID_NEWKEYS, WS_MSG_RECV)); + CHECK(DoNewKeys(ssh, NULL, 0, NULL) == WS_SUCCESS); + CHECK(ssh->isKeying == 0 && ssh->handshake == NULL); + CHECK(!IsMessageAllowed(ssh, MSGID_NEWKEYS, WS_MSG_RECV)); + CHECK(DoNewKeys(ssh, NULL, 0, NULL) == WS_BAD_ARGUMENT); + wolfSSH_free(ssh); + } + WOLFSSH* ssh = wolfSSH_new(ctx); + Sink sink = {.fatal = 1, .chunk = 1}; + wolfSSH_SetIOWriteCtx(ssh, &sink); + ssh->isKeying = WOLFSSH_SELF_IS_KEYING | WOLFSSH_PEER_IS_KEYING; + ssh->handshake->encryptId = ID_NONE; + CHECK(SendNewKeys(ssh) != WS_SUCCESS); + CHECK(ssh->isKeying & WOLFSSH_SELF_IS_KEYING); + wolfSSH_free(ssh); + + ssh = wolfSSH_new(ctx); + CHECK(ssh != NULL); + wolfSSH_SetIOWriteCtx(ssh, &sink); + ssh->isKeying = WOLFSSH_SELF_IS_KEYING | WOLFSSH_PEER_IS_KEYING; + ssh->handshake->encryptId = ID_AES128_GCM; + ssh->handshake->keys.encKeySz = 15; /* invalid AES key length */ + CHECK(SendNewKeys(ssh) != WS_SUCCESS); + CHECK(ssh->isKeying == (WOLFSSH_SELF_IS_KEYING | WOLFSSH_PEER_IS_KEYING)); + ssh->isKeying = WOLFSSH_PEER_IS_KEYING; + ssh->handshake->peerKeys.encKeySz = 15; + CHECK(DoNewKeys(ssh, NULL, 0, NULL) == WS_CRYPTO_FAILED); + CHECK(ssh->handshake != NULL && ssh->isKeying == WOLFSSH_PEER_IS_KEYING); + wolfSSH_free(ssh); +} + +static void shutdown_rekey(WOLFSSH_CTX* ctx) +{ + const int errors[] = {WS_SUCCESS, WS_WANT_WRITE}; + int keying, operation, pending, error; + for (keying = 1; keying <= 3; ++keying) { + for (operation = 0; operation < 3; ++operation) { + for (pending = 0; pending < 2; ++pending) { + for (error = 0; error < 2; ++error) { + WOLFSSH* ssh = wolfSSH_new(ctx); + Sink sink = {.quota = -1, .chunk = 1024}; + CHECK(ssh != NULL); + wolfSSH_SetIOWriteCtx(ssh, &sink); + WOLFSSH_CHANNEL* channel = ChannelNew(ssh, ID_CHANTYPE_SESSION, 1024, 1024); + CHECK(channel != NULL); + channel->peerChannel = channel->channel; + CHECK(ChannelAppend(ssh, channel) == WS_SUCCESS); + if (pending) { + sink.quota = 0; + ssh->isKeying = WOLFSSH_SELF_IS_KEYING | WOLFSSH_PEER_IS_KEYING; + CHECK(SendNewKeys(ssh) == WS_WANT_WRITE); + sink.quota = -1; + } + ssh->isKeying = keying; + ssh->handshake->expectMsgId = MSGID_NEWKEYS; + word32 length = ssh->outputBuffer.length; + word32 index = ssh->outputBuffer.idx, seq = ssh->seq; + byte saved[64]; + CHECK(length <= sizeof(saved)); + memcpy(saved, ssh->outputBuffer.buffer, length); + int calls = sink.calls; + int repeat; + for (repeat = 0; repeat < 2; ++repeat) { + ssh->error = errors[error]; /* Including a stale WANT_WRITE. */ + int ret = operation == 0 ? SendChannelEof(ssh, channel->peerChannel) : + operation == 1 ? wolfSSH_shutdown(ssh) : + SendChannelExit(ssh, channel->peerChannel, 0); + CHECK(ret == WS_MSGID_NOT_ALLOWED_E); + CHECK(ssh->error == WS_REKEYING); + CHECK(sink.size == 0 && sink.calls == calls); + CHECK(!channel->eofTxd && !channel->closeTxd); + CHECK(ssh->outputBuffer.length == length && ssh->outputBuffer.idx == index); + CHECK(memcmp(saved, ssh->outputBuffer.buffer, length) == 0); + CHECK(ssh->seq == seq && ssh->isKeying == keying); + CHECK(ssh->handshake->expectMsgId == MSGID_NEWKEYS); + } + /* Retry after keying: EOF is emitted once, not suppressed forever. */ + ssh->isKeying = 0; + ssh->error = WS_SUCCESS; + CHECK(SendChannelEof(ssh, channel->peerChannel) == WS_SUCCESS); + CHECK(channel->eofTxd && !channel->closeTxd); + CHECK(sink.size == length + 16 && sink.bytes[length + 5] == MSGID_CHANNEL_EOF); + calls = sink.calls; + CHECK(SendChannelEof(ssh, channel->peerChannel) == WS_SUCCESS); + CHECK(sink.calls == calls); + wolfSSH_free(ssh); + } + } + } + } + puts("PASS: real EOF/shutdown/exit rekey fences, pending output/stale WANT_WRITE, zero bytes and no channel mutation"); +} + +#include "interop.c" +#include "paired.c" + +int main(int argc, char** argv) +{ + if (argc > 1) return serve(argc, argv); + CHECK(wolfSSH_Init() == WS_SUCCESS); + WOLFSSH_CTX* ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_SERVER, NULL); + CHECK(ctx != NULL); + policy(ctx); + wolfSSH_SetIOSend(ctx, send_test); + WOLFSSH* ssh = wolfSSH_new(ctx); + CHECK(ssh != NULL && ssh->handshake != NULL); + gates(ssh); + wolfSSH_free(ssh); + newkeys(ctx); + shutdown_rekey(ctx); + paired(); + wolfSSH_CTX_free(ctx); + wolfSSH_Cleanup(); + printf("PASS: %u real generated ordering/dispatch/NEWKEYS checks\n", checks); + return 0; +} diff --git a/tests/wolfssh_order_contract/interop.c b/tests/wolfssh_order_contract/interop.c new file mode 100644 index 0000000..b0491b4 --- /dev/null +++ b/tests/wolfssh_order_contract/interop.c @@ -0,0 +1,175 @@ +/* Test-only stdio server; no sockets, persistent keys, or production auth. */ +#include +#include +#include +static void trap_trace(int signum) +{ + void* trace[32]; + int count = backtrace(trace, 32); + backtrace_symbols_fd(trace, count, 2); + _exit(128 + signum); +} + +static byte authorized[2048]; +static size_t authorizedSz; +static int keyings, signedAuth, passwordAuth, writes, stalls, fragment, transportEof; + +static int auth_test(byte type, WS_UserAuthData* data, void* context) +{ + (void)context; + if (type == WOLFSSH_USERAUTH_PASSWORD && + data->sf.password.passwordSz == 19 && + memcmp(data->sf.password.password, "order-test-password", 19) == 0) { + ++passwordAuth; + return WOLFSSH_USERAUTH_SUCCESS; + } + if (type == WOLFSSH_USERAUTH_PUBLICKEY && + data->sf.publicKey.publicKeySz == authorizedSz && + memcmp(data->sf.publicKey.publicKey, authorized, authorizedSz) == 0) { + if (data->sf.publicKey.hasSignature) ++signedAuth; + return WOLFSSH_USERAUTH_SUCCESS; + } + return WOLFSSH_USERAUTH_FAILURE; +} + +static void keyed_test(void* context) { (void)context; ++keyings; } + +static int stdio_recv(WOLFSSH* ssh, void* buffer, word32 size, void* context) +{ + (void)ssh; (void)context; + if (fragment && size > 31) size = 31; + int ret = (int)read(0, buffer, size); + if (ret > 0) return ret; + if (ret < 0 && (errno == EAGAIN || errno == EINTR)) + return WS_CBIO_ERR_WANT_READ; + if (ret == 0) transportEof = 1; + return WS_CBIO_ERR_CONN_CLOSE; +} + +static int stdio_send(WOLFSSH* ssh, void* buffer, word32 size, void* context) +{ + (void)ssh; (void)context; + if (fragment && ++writes % 3 == 0) { + ++stalls; + return WS_CBIO_ERR_WANT_WRITE; + } + if (fragment && size > 37) size = 37; + int ret = (int)write(1, buffer, size); + if (ret > 0) return ret; + if (ret < 0 && (errno == EAGAIN || errno == EINTR)) + return WS_CBIO_ERR_WANT_WRITE; + return WS_CBIO_ERR_GENERAL; +} + +static int retry(int ret, WOLFSSH* ssh) +{ + int error = ret == WS_FATAL_ERROR ? wolfSSH_get_error(ssh) : ret; + if (error == WS_WANT_READ || error == WS_WANT_WRITE || error == WS_REKEYING || + error == WS_CHAN_RXD || error == WS_SUCCESS) return 1; + fprintf(stderr, "server failure ret=%d err=%d accept=%d keying=%d expected=%d\n", + ret, error, ssh->acceptState, ssh->isKeying, + ssh->handshake ? ssh->handshake->expectMsgId : -1); + return 0; +} + +static int serve(int argc, char** argv) +{ + byte key[4096], buffer[4096]; + size_t keySz, sent = 0, length = 0, total = 0; + int triggered = 0, channelClosed = 0; + CHECK(argc == 6); + fragment = atoi(argv[4]); + int initiate = atoi(argv[5]); + alarm(30); + signal(SIGILL, trap_trace); + signal(SIGSEGV, trap_trace); + FILE* file = fopen(argv[2], "rb"); CHECK(file != NULL); + keySz = fread(key, 1, sizeof(key), file); fclose(file); + file = fopen(argv[3], "rb"); CHECK(file != NULL); + authorizedSz = fread(authorized, 1, sizeof(authorized), file); fclose(file); + CHECK(fcntl(0, F_SETFL, O_NONBLOCK) == 0); + CHECK(fcntl(1, F_SETFL, O_NONBLOCK) == 0); + CHECK(wolfSSH_Init() == 0); + WOLFSSH_CTX* ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_SERVER, NULL); + CHECK(ctx != NULL); + policy(ctx); + CHECK(wolfSSH_CTX_UsePrivateKey_buffer(ctx, key, keySz, WOLFSSH_FORMAT_ASN1) == 0); + wolfSSH_SetUserAuth(ctx, auth_test); + wolfSSH_SetKeyingCompletionCb(ctx, keyed_test); + wolfSSH_SetIORecv(ctx, stdio_recv); + wolfSSH_SetIOSend(ctx, stdio_send); + WOLFSSH* ssh = wolfSSH_new(ctx); CHECK(ssh != NULL); + int ret; + while ((ret = wolfSSH_accept(ssh)) != WS_SUCCESS) { + CHECK(retry(ret, ssh)); + usleep(100); + } + CHECK(keyings == 1 && (signedAuth == 1 || passwordAuth == 1)); + CHECK(ssh->sendExtInfo == 0 && ssh->extInfoSent == 0); + for (;;) { + if (ssh->outputBuffer.length) { + ret = wolfSSH_SendPacket(ssh); + CHECK(retry(ret, ssh)); + if (ret != WS_SUCCESS) { usleep(100); continue; } + } + if (initiate && !triggered && total >= 65536 && !ssh->isKeying) { + triggered = 1; + ret = wolfSSH_TriggerKeyExchange(ssh); + CHECK(retry(ret, ssh)); + } + if (ssh->isKeying) { + ret = wolfSSH_worker(ssh, NULL); + CHECK(retry(ret, ssh)); + usleep(100); + continue; + } + if (sent < length) { + ret = wolfSSH_stream_send(ssh, buffer + sent, length - sent); + if (ret > 0) { sent += ret; total += ret; } + else { CHECK(retry(ret, ssh)); usleep(100); } + continue; + } + ret = wolfSSH_stream_read(ssh, buffer, sizeof(buffer)); + if (ret == WS_EOF || (ssh->channelList && ssh->channelList->eofRxd)) break; + if (ret > 0) { length = ret; sent = 0; } + else { CHECK(retry(ret, ssh)); usleep(100); } + } + CHECK(total == 262144); + CHECK(keyings >= 2); + CHECK(!fragment || stalls > 0); + CHECK(ssh->sendExtInfo == 0 && ssh->extInfoSent == 0); + /* Queue shutdown once, then finish IO without re-enqueuing exit-status. + * WANT_READ is not channel-close completion. Keep the proxy read end alive + * until OpenSSH has also finished writing its transport disconnect. */ + fragment = 0; + ret = wolfSSH_shutdown(ssh); + if (ret == WS_CHANNEL_CLOSED) channelClosed = 1; + else CHECK(retry(ret, ssh)); + while (ssh->channelList || ssh->outputBuffer.length) { + ret = ssh->outputBuffer.length ? wolfSSH_SendPacket(ssh) : + wolfSSH_worker(ssh, NULL); + if (ret == WS_CHANNEL_CLOSED) { + CHECK(ssh->channelList == NULL); + channelClosed = 1; + } + else CHECK(retry(ret, ssh)); + usleep(100); + } + CHECK(channelClosed && !transportEof); + while (!transportEof) { + ret = wolfSSH_worker(ssh, NULL); + if (transportEof) { + int error = ret == WS_FATAL_ERROR ? wolfSSH_get_error(ssh) : ret; + CHECK(error == WS_SOCKET_ERROR_E && ssh->isClosed && !ssh->connReset); + } + else CHECK(retry(ret, ssh)); + usleep(100); + } + CHECK(!ssh->isKeying && !ssh->outputBuffer.length); + fprintf(stderr, "INTEROP PASS keyings=%d signed=%d password=%d stalls=%d bytes=%zu channel_closed=1 transport_eof=1\n", + keyings, signedAuth, passwordAuth, stalls, total); + wolfSSH_free(ssh); + wolfSSH_CTX_free(ctx); + wolfSSH_Cleanup(); + return 0; +} diff --git a/tests/wolfssh_order_contract/paired.c b/tests/wolfssh_order_contract/paired.c new file mode 100644 index 0000000..3f115dd --- /dev/null +++ b/tests/wolfssh_order_contract/paired.c @@ -0,0 +1,115 @@ +/* Real generated client/server state machines over bounded nonblocking queues. */ +typedef struct Pipe { + byte bytes[65536]; + word32 read, length; + unsigned calls, stalls; +} Pipe; + +static int pipe_send(WOLFSSH* ssh, void* data, word32 size, void* context) +{ + Pipe* pipe = context; + (void)ssh; + if (++pipe->calls % 3 == 0) { + ++pipe->stalls; + return WS_CBIO_ERR_WANT_WRITE; + } + if (size > 13) size = 13; + CHECK(size <= sizeof(pipe->bytes) - pipe->length); + memcpy(pipe->bytes + pipe->length, data, size); + pipe->length += size; + return size; +} + +static int pipe_recv(WOLFSSH* ssh, void* data, word32 size, void* context) +{ + Pipe* pipe = context; + (void)ssh; + word32 available = pipe->length - pipe->read; + if (!available) return WS_CBIO_ERR_WANT_READ; + if (size > available) size = available; + if (size > 11) size = 11; + memcpy(data, pipe->bytes + pipe->read, size); + pipe->read += size; + if (pipe->read == pipe->length) pipe->read = pipe->length = 0; + return size; +} + +static int client_auth(byte type, WS_UserAuthData* data, void* context) +{ + (void)context; + if (type != WOLFSSH_USERAUTH_PASSWORD) return WOLFSSH_USERAUTH_FAILURE; + data->sf.password.password = (const byte*)"order-test-password"; + data->sf.password.passwordSz = 19; + return WOLFSSH_USERAUTH_SUCCESS; +} + +static void paired(void) +{ + const char* algorithms[] = {"curve25519-sha256", "ecdh-sha2-nistp256"}; + int algorithm; + for (algorithm = 0; algorithm < 2; ++algorithm) { + WOLFSSH_CTX* serverCtx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_SERVER, NULL); + WOLFSSH_CTX* clientCtx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_CLIENT, NULL); + CHECK(serverCtx && clientCtx); + policy(serverCtx); policy(clientCtx); + CHECK(wolfSSH_CTX_SetAlgoListKex(clientCtx, algorithms[algorithm]) == 0); + CHECK(wolfSSH_CTX_SetAlgoListCipher(clientCtx, "aes256-gcm@openssh.com") == 0); + WC_RNG rng; + ecc_key key; + byte der[1024]; + CHECK(wc_InitRng(&rng) == 0); + CHECK(wc_ecc_init(&key) == 0); + CHECK(wc_ecc_make_key(&rng, 32, &key) == 0); + int derSz = wc_EccKeyToDer(&key, der, sizeof(der)); + CHECK(derSz > 0); + CHECK(wolfSSH_CTX_UsePrivateKey_buffer(serverCtx, der, derSz, WOLFSSH_FORMAT_ASN1) == 0); + wc_ecc_free(&key); wc_FreeRng(&rng); + wolfSSH_SetUserAuth(serverCtx, auth_test); + wolfSSH_SetUserAuth(clientCtx, client_auth); + wolfSSH_SetIOSend(serverCtx, pipe_send); wolfSSH_SetIORecv(serverCtx, pipe_recv); + wolfSSH_SetIOSend(clientCtx, pipe_send); wolfSSH_SetIORecv(clientCtx, pipe_recv); + WOLFSSH* server = wolfSSH_new(serverCtx); + WOLFSSH* client = wolfSSH_new(clientCtx); + Pipe toServer = {0}, toClient = {0}; + CHECK(server && client); + CHECK(wolfSSH_SetUsername(client, "order-test") == 0); + wolfSSH_SetIOWriteCtx(client, &toServer); wolfSSH_SetIOReadCtx(server, &toServer); + wolfSSH_SetIOWriteCtx(server, &toClient); wolfSSH_SetIOReadCtx(client, &toClient); + int clientDone = 0, serverDone = 0, i, ret; + for (i = 0; i < 100000 && !(clientDone && serverDone); ++i) { + if (!clientDone) { + ret = wolfSSH_connect(client); + if (ret == 0) clientDone = 1; + else CHECK(retry(ret, client)); + } + if (!serverDone) { + ret = wolfSSH_accept(server); + if (ret == 0) serverDone = 1; + else CHECK(retry(ret, server)); + } + } + CHECK(clientDone && serverDone); + CHECK(client->handshake == NULL && server->handshake == NULL); + CHECK(client->sendExtInfo == 0 && server->sendExtInfo == 0); + CHECK(client->peerSigId == NULL && client->peerSigIdSz == 0); + /* Both initiation directions and simultaneous initiation. */ + int round; + for (round = 0; round < 3; ++round) { + if (round != 1) CHECK(retry(wolfSSH_TriggerKeyExchange(server), server)); + if (round != 0) CHECK(retry(wolfSSH_TriggerKeyExchange(client), client)); + for (i = 0; i < 100000; ++i) { + CHECK(retry(wolfSSH_worker(server, NULL), server)); + CHECK(retry(wolfSSH_worker(client, NULL), client)); + if (!server->isKeying && !client->isKeying && + !toServer.length && !toClient.length) break; + } + CHECK(i < 100000); + CHECK(client->handshake == NULL && server->handshake == NULL); + CHECK(client->sendExtInfo == 0 && server->sendExtInfo == 0); + } + CHECK(toServer.stalls > 0 && toClient.stalls > 0); + wolfSSH_free(client); wolfSSH_free(server); + wolfSSH_CTX_free(clientCtx); wolfSSH_CTX_free(serverCtx); + } + puts("PASS: real generated client/server, both KEX, AES256, fragmented initial KEX and three rekey directions"); +} diff --git a/tests/wolfssh_order_contract/pio_adapter.py b/tests/wolfssh_order_contract/pio_adapter.py new file mode 100644 index 0000000..b730f54 --- /dev/null +++ b/tests/wolfssh_order_contract/pio_adapter.py @@ -0,0 +1,105 @@ +#!/usr/bin/env python3 +"""Exercise installed PlatformIO ESP-IDF flag sorting and SCons deduplication. + +Uses the real configured CMake file API and target compiler. Never runs pio or +writes build artifacts; compiler outputs and the split-option mutation are temp. +""" +import ast +import json +import os +from pathlib import Path +import shlex +import subprocess +import sys +import tempfile +import click + +ROOT = Path(__file__).resolve().parents[2] + + +def main(): + platformio = Path.home() / '.platformio' + adapter = platformio / 'platforms/espressif32/builder/frameworks/espidf.py' + tree = ast.parse(adapter.read_text()) + functions = [node for node in tree.body if isinstance(node, ast.FunctionDef) + and node.name == 'get_app_flags'] + assert len(functions) == 1 + scope = {'click': click} + exec(compile(ast.Module(body=functions, type_ignores=[]), str(adapter), 'exec'), scope) + scons = list((platformio / 'packages/tool-scons').glob('scons-local-*')) + assert len(scons) == 1 + sys.path.insert(0, str(scons[0])) + from SCons.Script import Environment + + build = ROOT / '.pio/build/esp32-s3-devkitc-1-n16r8' + replies = build / '.cmake/api/v1/reply' + targets = [json.loads(path.read_text()) for path in replies.glob('target-*.json')] + matches = [target for target in targets if target['name'] == '__idf_src'] + assert len(matches) == 1, 'requires configured PlatformIO app target' + app = matches[0] + groups = [group for group in app['compileGroups'] if group['language'] == 'C'] + assert len(groups) == 1 + group = groups[0] + defaults = {'compileGroups': [{'language': lang, 'compileCommandFragments': []} + for lang in ('C', 'CXX', 'ASM')]} + commands = json.loads((build / 'compile_commands.json').read_text()) + main_command = next(entry for entry in commands if entry['file'].endswith('/src/main.c')) + compiler = (main_command.get('arguments') or shlex.split(main_command['command']))[0] + overlay = str(build / 'security_overrides/wolfssh_include/wolfssh/internal.h') + crypto = str(ROOT / 'cmake/wolf_crypto_policy.h') + defines = ['-D' + item['define'] for item in group.get('defines', [])] + includes = ['-I' + item['path'] for item in group.get('includes', [])] + flags = scope['get_app_flags'](app, defaults)['CFLAGS'] + for header in (overlay, crypto): + assert '-include' + header in flags, header + assert header not in flags, 'orphan header operand' + assert '-include' not in flags + env = Environment(tools=[]) + env.AppendUnique(CFLAGS=flags) + env.AppendUnique(CFLAGS=flags) + assert list(env['CFLAGS']) == flags + # Component path uses ParseFlags/AppendUnique rather than get_app_flags. + component_env = Environment(tools=[]) + for header in (crypto, overlay, crypto, overlay): + component_env.AppendUnique(**component_env.ParseFlags('-include' + header)) + assert list(component_env['CCFLAGS']) == ['-include' + crypto, '-include' + overlay] + + with tempfile.TemporaryDirectory(prefix='pio-forced-include-') as temp: + temp = Path(temp) + source = temp / 'consumer.c' + source.write_text('''#if SAK_WOLFSSH_ORDER_ABI != 20260916 +#error missing_shared_ssh_ABI +#endif +#if !defined(WOLFSSL_VALIDATE_ECC_IMPORT) || !defined(WOLFSSL_ECDHX_SHARED_NOT_ZERO) +#error missing_crypto_guards +#endif +int consumer(void) { return 0; } +''') + command = [compiler, *env['CFLAGS'], *defines, *includes, '-c', str(source), '-o', str(temp / 'consumer.o')] + result = subprocess.run(command, cwd=ROOT, capture_output=True, text=True, timeout=30, + env={**os.environ, 'CCACHE_DISABLE': '1'}) + assert result.returncode == 0, result.stderr + assert (temp / 'consumer.o').is_file() + + # Prove this catches the historical split-option failure using the same + # installed adapter, not a hand-written approximation of its sorting. + bad = json.loads(json.dumps(app)) + count = 0 + for cg in bad['compileGroups']: + for fragment in cg['compileCommandFragments']: + text = fragment['fragment'] + if '-include' + overlay in text: + fragment['fragment'] = text.replace('-include' + overlay, '-include ' + overlay) + count += 1 + assert count > 0 + broken_flags = scope['get_app_flags'](bad, defaults)['CFLAGS'] + result = subprocess.run([compiler, *broken_flags, *defines, *includes, '-c', str(source), + '-o', str(temp / 'broken.o')], cwd=ROOT, capture_output=True, + text=True, timeout=30, env={**os.environ, 'CCACHE_DISABLE': '1'}) + assert result.returncode != 0 and 'multiple files' in result.stderr, result.stderr + print('PASS: installed PlatformIO get_app_flags + SCons AppendUnique/ParseFlags; ' + 'real Xtensa -c/-o consumer; split-include mutation reproduces multiple-input failure') + + +if __name__ == '__main__': + main() diff --git a/tests/wolfssh_order_contract/run.py b/tests/wolfssh_order_contract/run.py new file mode 100644 index 0000000..7092f4d --- /dev/null +++ b/tests/wolfssh_order_contract/run.py @@ -0,0 +1,314 @@ +#!/usr/bin/env python3 +"""Offline real generated wolfSSH/real wolfCrypt ordering and interoperability tests. + +No PlatformIO, device, managed writes, or IP network. OpenSSH uses a local Unix socket. +""" +import argparse +import array +import socket +import hashlib +import base64 +import shlex +import resource +import re +import json +import os +from pathlib import Path +import subprocess +import sys +import tempfile + +sys.dont_write_bytecode = True +ROOT = Path(__file__).resolve().parents[2] +HERE = Path(__file__).resolve().parent +sys.path.insert(0, str(ROOT / 'tools')) +import security_overrides as sdk +SSH = ROOT / 'managed_components/wolfssl__wolfssh' +SSL = ROOT / 'managed_components/wolfssl__wolfssl' +ENV = {**os.environ, 'CCACHE_DISABLE': '1'} + + +def run(args, **kw): + return subprocess.run([str(x) for x in args], check=True, timeout=120, + env=ENV, **kw) + + +def provenance(): + directory = ROOT / 'tools/wolfssh_order' + records = json.loads((directory / 'provenance.json').read_text()) + assert set(records) == {'793', '819', '840', '855', '921'} + for number, record in records.items(): + raw = (directory / (number + '.patch')).read_bytes() + assert hashlib.sha256(raw).hexdigest() == record['sha256'] + assert re.findall(rb'^From ([0-9a-f]{40}) Mon Sep', raw, re.M) == [ + commit.encode() for commit in record['commits']] + assert set(sdk.WOLFSSH_ORDER_PLAN) == {'src/internal.c', 'src/ssh.c', 'wolfssh/internal.h'} + for path, record in sdk.WOLFSSH_ORDER_PLAN.items(): + entry = next(e for e in sdk.ENTRIES if e.source == 'managed_components/wolfssl__wolfssh/' + path) + assert record['sha256'] == entry.sha256 + assert hashlib.sha256((SSH / 'src/log.c').read_bytes()).hexdigest() == \ + '66e5f053af05a103aca997fbcae11a52acd8f79c9c97645f0d0cd390e192135a' + assert hashlib.sha256((SSH / 'wolfssh/log.h').read_bytes()).hexdigest() == \ + '4a3e71f8148b0e6ecd533b949e2f9574449561bda21bc200163a1e53bfdb0bd2' + print('PASS: archived upstream patch hashes/commits; exact source/header pins; unchanged logging ABI') + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--interop', action='store_true') + parser.add_argument('--interop-repeat', type=int, default=1) + parser.add_argument('--target-contracts', action='store_true') + parser.add_argument('--pio-adapter', action='store_true') + parser.add_argument('--proxy-socket', type=Path, help=argparse.SUPPRESS) + options = parser.parse_args() + if options.proxy_socket: + with socket.socket(socket.AF_UNIX) as connection: + connection.connect(str(options.proxy_socket)) + with socket.socket(fileno=1) as output: + output.sendmsg([b'\0'], [(socket.SOL_SOCKET, socket.SCM_RIGHTS, + array.array('i', [connection.fileno()]))]) + return + if not 1 <= options.interop_repeat <= 20: + parser.error('--interop-repeat must be between 1 and 20') + provenance() + resource.setrlimit(resource.RLIMIT_CORE, (0, 0)) + with tempfile.TemporaryDirectory(prefix='wolfssh-order-') as tmp: + work = Path(tmp) + for entry in sdk.ENTRIES: + if entry.component != 'wolfssl__wolfssh': + continue + _, data = sdk.render_entry(entry, {'project': ROOT}) + dest = (work / 'wolfssh/internal.h' if entry.header else + work / Path(entry.source).name) + dest.parent.mkdir(parents=True, exist_ok=True) + dest.write_bytes(data) + flags = ['cc', '-std=gnu11', '-O1', '-g', '-DWOLFSSL_USER_SETTINGS', + '-DHAVE_WC_ECC_SET_RNG', '-ffunction-sections', '-fdata-sections', + '-fsanitize=undefined', '-fsanitize-undefined-trap-on-error', '-no-pie', + '-I' + str(HERE), '-I' + str(SSH), '-I' + str(work), '-I' + str(SSL), + '-include', str(work / 'wolfssh/internal.h')] + stale = subprocess.run([*flags[:-2], '-include', str(SSH / 'wolfssh/internal.h'), + *flags[-2:], '-x', 'c', '-fsyntax-only', '-'], + input='', text=True, capture_output=True, timeout=30, env=ENV) + assert stale.returncode != 0 and 'stale wolfSSH internal.h' in stale.stderr + print('PASS: vendor-first forced-header ABI conflict rejected') + crypto = ('aes asn coding curve25519 ecc ed25519 fe_low_mem ge_low_mem ' + 'hash hmac kdf logging memory random sha256 sha512 signature ' + 'tfm wc_port wolfmath').split() + sources = [work / 'ssh.c', *[SSH / 'src' / (s + '.c') for s in ('io', 'log', 'port')], + *[SSL / 'wolfcrypt/src' / (s + '.c') for s in crypto]] + objects = [] + for i, source in enumerate(sources): + obj = work / f'{i}.o' + run([*flags, '-c', source, '-o', obj], capture_output=True) + objects.append(obj) + binary = work / 'contract' + run([*flags, HERE / 'contract.c', *objects, '-Wl,--gc-sections', '-o', binary]) + run([binary]) + mutations(work, flags, objects) + if options.interop: + for iteration in range(options.interop_repeat): + print(f'INTEROP matrix {iteration + 1}/{options.interop_repeat}', flush=True) + interop(work, binary) + if options.target_contracts: + target_contracts(work) + if options.pio_adapter: + run([sys.executable, HERE / 'pio_adapter.py']) + print('PASS: generated pinned source/header ordering profile (host, not firmware)') + + +def target_contracts(work): + """Explicit candidate replay, not a firmware reconfigure/build claim.""" + original_db = ROOT / '.pio/build/esp32-s3-devkitc-1-n16r8/compile_commands.json' + entries = json.loads(original_db.read_text()) + replacements = {} + for entry in sdk.ENTRIES: + if entry.component != 'wolfssl__wolfssh': + continue + _, data = sdk.render_entry(entry, {'project': ROOT}) + dest = (work / 'security_overrides/wolfssh_include/wolfssh/internal.h' if entry.header + else work / 'security_overrides' / entry.name / Path(entry.source).name) + dest.parent.mkdir(parents=True, exist_ok=True) + dest.write_bytes(data) + if entry.header: + overlay = dest + else: + replacements[entry.name] = dest + for entry in entries: + source = Path(entry['file']) + if not source.is_absolute(): + source = Path(entry['directory']) / source + args = entry.get('arguments') or shlex.split(entry['command']) + dest = None + if source.name == 'internal.c' and 'wolfssh_internal' in source.parts: + dest = replacements['wolfssh_internal'] + if source.name == 'ssh.c' and ('wolfssl__wolfssh' in source.parts or 'wolfssh_ssh' in source.parts): + dest = replacements['wolfssh_ssh'] + if dest: + args = [str(dest) if arg == entry['file'] or arg == str(source) else arg for arg in args] + entry['file'] = str(dest) + args = [(('-include' if arg.startswith('-include') else '') + str(overlay)) + if arg.endswith('/security_overrides/wolfssh_include/wolfssh/internal.h') + else arg for arg in args] + # Same ABI overlay for all candidate consumers. Original compile flags, + # crypto policy and feature settings are preserved, not synthesized. + if any(arg.startswith('-I') and 'wolfssl__wolfssh' in arg for arg in args): + args += ['-include' + str(overlay)] + entry['arguments'] = args + entry.pop('command', None) + database = work / 'candidate-compile_commands.json' + database.write_text(json.dumps(entries)) + print('CANDIDATE ONLY: replaying existing target flags with freshly generated SSH sources/header; ' + 'no production build-tree changes', flush=True) + syntax_sources = [] + for entry in entries: + if (entry['file'] not in map(str, replacements.values()) and + not entry['file'].endswith(('/src/ssh_transport.c', '/src/ssh_security.c'))): + continue + command = [] + skip = False + for arg in entry['arguments']: + if skip: + skip = False + elif arg in ('-o', '-MF', '-MT', '-MQ'): + skip = True + elif arg not in ('-c', '-MD', '-MMD', '-MP'): + command.append(arg) + run([*command, '-fsyntax-only'], cwd=entry['directory'], capture_output=True) + syntax_sources.append(entry['file']) + assert len(syntax_sources) == len(set(syntax_sources)) == 4, syntax_sources + print('PASS: candidate Xtensa syntax for generated internal.c/ssh.c and SSH application consumers') + for suite in ('ssh_protocol_policy', 'wolf_crypto_policy', 'wolfssh_auth_contract'): + run([sys.executable, ROOT / 'tests' / suite / 'run.py', '--compile-commands', database]) + print('PASS: candidate target source-contract replay; NOT firmware/build registration evidence') + + +def mutations(work, flags, objects): + source = work / 'internal.c' + original = source.read_text() + cases = ( + ('missing EOF rekey guard', + ' if (ret == WS_SUCCESS) {\n' + ' if (!IsMessageAllowed(ssh, MSGID_CHANNEL_EOF, WS_MSG_SEND)) {\n' + ' ret = WS_MSGID_NOT_ALLOWED_E;\n }\n }\n\n', ''), + ('preauth injection', ' if (state != WS_MSG_RECV)\n', + ' if (msg == MSGID_USERAUTH_FAILURE) return 1;\n if (state != WS_MSG_RECV)\n'), + ('wrong expected KEX', 'ssh->handshake->expectMsgId == msg)', + 'ssh->handshake->expectMsgId != msg)'), + ('unnegotiated EXT_INFO', 'if (msg == MSGID_EXT_INFO)\n goto reject;', + 'if (msg == MSGID_EXT_INFO)\n return 1;'), + ('NEWKEYS clears peer too', 'ssh->isKeying &= ~WOLFSSH_SELF_IS_KEYING;', + 'ssh->isKeying = 0;'), + ('peer NEWKEYS before local', '(ssh->isKeying & WOLFSSH_SELF_IS_KEYING) ||\n ', ''), + ('PR921 missing server expectation', + ' if (ret == WS_SUCCESS) {\n ssh->handshake->expectMsgId = MSGID_NEWKEYS;\n' + ' WLOG_EXPECT_MSGID(ssh->handshake->expectMsgId);\n ret = SendNewKeys(ssh);\n }\n\n' + ' if (ret != WS_WANT_WRITE && ret != WS_SUCCESS)', + ' if (ret == WS_SUCCESS) {\n ret = SendNewKeys(ssh);\n }\n\n' + ' if (ret != WS_WANT_WRITE && ret != WS_SUCCESS)'), + ) + try: + for label, old, new in cases: + assert original.count(old) == 1, label + source.write_text(original.replace(old, new)) + binary = work / 'mutation' + run([*flags, HERE / 'contract.c', *objects, '-Wl,--gc-sections', '-o', binary], capture_output=True) + result = subprocess.run([str(binary)], capture_output=True, timeout=30, env=ENV) + assert result.returncode != 0, 'undetected ordering mutation: ' + label + finally: + source.write_text(original) + print(f'PASS: {len(cases)} ordering/NEWKEYS/PR921 guard-removal mutations rejected') + + +def interop(work, binary): + run(['openssl', 'ecparam', '-name', 'prime256v1', '-genkey', '-noout', + '-outform', 'DER', '-out', work / 'host.der'], capture_output=True) + for kind in ('ed25519', 'ecdsa'): + (work / kind).unlink(missing_ok=True) + (work / (kind + '.pub')).unlink(missing_ok=True) + run(['ssh-keygen', '-q', '-t', kind, '-N', '', '-C', 'order-test', + '-f', work / kind], capture_output=True) + askpass = work / 'askpass' + askpass.write_text('#!/bin/sh\nprintf "%s\\n" "order-test-password"\n') + askpass.chmod(0o700) + payload = bytes(range(256)) * 1024 + for kex in ('curve25519-sha256', 'ecdh-sha2-nistp256'): + for auth in ('ed25519', 'ecdsa', 'password'): + key = work / ('ecdsa' if auth == 'password' else auth) + (work / 'authorized').write_bytes(base64.b64decode( + key.with_suffix('.pub').read_text().split()[1])) + for fragment, initiate in ((0, 0), (1, 1)): + endpoint = work / 'interop.sock' + proxy = ' '.join(shlex.quote(str(x)) for x in + (sys.executable, HERE / 'run.py', '--proxy-socket', endpoint)) + server_args = [str(x) for x in + (binary, '--stdio', work / 'host.der', work / 'authorized', fragment, initiate)] + args = ['ssh', '-F', '/dev/null', '-T', '-vv', '-o', 'StrictHostKeyChecking=no', + '-o', 'UserKnownHostsFile=/dev/null', '-o', 'GlobalKnownHostsFile=/dev/null', + '-o', 'IdentityAgent=none', '-o', 'IdentitiesOnly=yes', + '-o', 'HostKeyAlgorithms=ecdsa-sha2-nistp256', + '-o', 'KexAlgorithms=' + kex, '-o', 'Ciphers=aes128-gcm@openssh.com', + '-o', 'RekeyLimit=' + ('1G' if initiate else '32K'), + '-o', 'ProxyCommand=' + proxy, '-o', 'ProxyUseFdpass=yes', + '-o', 'NumberOfPasswordPrompts=1', + '-o', 'PreferredAuthentications=' + ('password' if auth == 'password' else 'publickey'), + '-i', str(key), 'order-test@stdio.invalid'] + env = {**ENV, 'SSH_ASKPASS': str(askpass), 'SSH_ASKPASS_REQUIRE': 'force', 'DISPLAY': ':order-test'} + # Own/reap the server independently: OpenSSH kills its proxy at + # exit. The proxy only passes a local Unix socket, not the server. + with socket.socket(socket.AF_UNIX) as listener, tempfile.TemporaryFile() as server_log: + listener.bind(str(endpoint)) + listener.listen(1) + listener.settimeout(10) + client = subprocess.Popen(args, stdin=subprocess.PIPE, stdout=subprocess.PIPE, + stderr=subprocess.PIPE, env=env) + server = None + try: + connection, _ = listener.accept() + with connection: + server = subprocess.Popen(server_args, stdin=connection, stdout=connection, + stderr=server_log, env=env) + stdout, stderr = client.communicate(payload, timeout=45) + server_rc = server.wait(timeout=5) + finally: + for process in (client, server): + if process is not None and process.poll() is None: + process.kill() + process.wait(timeout=5) + endpoint.unlink(missing_ok=True) + server_log.seek(0) + server_text = server_log.read().decode(errors='replace') + result = subprocess.CompletedProcess(args, client.returncode, stdout, stderr) + client_log = stderr.decode(errors='replace') + log = client_log + '\nSERVER:\n' + server_text + evidence = re.fullmatch( + r'INTEROP PASS keyings=(\d+) signed=(\d+) password=(\d+) stalls=(\d+) ' + r'bytes=262144 channel_closed=1 transport_eof=1\n', server_text) + valid = (evidence is not None and int(evidence[1]) >= 2 and + (int(evidence[2]), int(evidence[3])) == + ((0, 1) if auth == 'password' else (1, 0)) and + (not fragment or int(evidence[4]) > 0) and + 'rtype exit-status reply 0' in client_log and + 'channel 0: rcvd close' in client_log and + 'Exit status 0' in client_log) + if result.returncode or server_rc or result.stdout != payload or not valid: + addresses = re.findall(r'\[(0x[0-9a-f]+)\]', log) + if addresses: + run(['addr2line', '-f', '-e', binary, *addresses]) + raise AssertionError(f'{kex}/{auth}/{fragment}/{initiate}: rc={result.returncode} server_rc={server_rc} ' + f'output={len(result.stdout)}\n{log}') + assert 'SSH2_MSG_EXT_INFO received' not in log + assert 'server-sig-algs=<' not in log + print(f'PASS: OpenSSH {kex}/{auth} fragment={fragment} server-rekey={initiate}; ' + + next(line for line in log.splitlines() if 'INTEROP PASS' in line)) + + +if __name__ == '__main__': + try: + main() + except subprocess.CalledProcessError as error: + if error.stdout: + print(error.stdout.decode() if isinstance(error.stdout, bytes) else error.stdout) + if error.stderr: + print(error.stderr.decode() if isinstance(error.stderr, bytes) else error.stderr) + raise diff --git a/tests/wolfssh_order_contract/user_settings.h b/tests/wolfssh_order_contract/user_settings.h new file mode 100644 index 0000000..cc14a08 --- /dev/null +++ b/tests/wolfssh_order_contract/user_settings.h @@ -0,0 +1,43 @@ +/* Host test profile: real generated wolfSSH + real wolfCrypt, not ESP-IDF. */ +#ifndef ORDER_TEST_SETTINGS_H +#define ORDER_TEST_SETTINGS_H +#include +#define WOLFCRYPT_ONLY +#define WOLFSSL_WOLFSSH +/* Use the portable bytewise encoding path under alignment sanitization. */ +#define WOLFSSL_USE_ALIGN +#define SINGLE_THREADED +#define USE_FAST_MATH +#define TFM_NO_ASM +#define TFM_TIMING_RESISTANT +#define WOLFSSL_SMALL_STACK +#define NO_RSA +#define NO_DH +#define NO_DSA +#define NO_MD5 +#define NO_SHA +#define NO_SHA224 +#define NO_DES3 +#define NO_RC4 +#define NO_HC128 +#define NO_RABBIT +#define NO_PSK +#define NO_PWDBASED +#define NO_PKCS12 +#define NO_CERTS +#define WOLFSSL_ASN_TEMPLATE +#define HAVE_ECC +#define ECC_TIMING_RESISTANT +#define HAVE_ECC_CHECK_KEY +#define WOLFSSL_VALIDATE_ECC_IMPORT +#define HAVE_CURVE25519 +#define CURVE25519_SMALL +#define WOLFSSL_ECDHX_SHARED_NOT_ZERO +#define HAVE_ED25519 +#define ED25519_SMALL +#define WOLFSSL_SHA512 +#define WOLFSSL_ED25519_STREAMING_VERIFY +#define HAVE_AESGCM +#define WOLFSSH_NO_AES_CBC +#define WOLFSSH_NO_AES_CTR +#endif diff --git a/tests/wolfssh_parser_contract/run.py b/tests/wolfssh_parser_contract/run.py index f446d0c..17be72c 100644 --- a/tests/wolfssh_parser_contract/run.py +++ b/tests/wolfssh_parser_contract/run.py @@ -20,9 +20,10 @@ original, generated = render_entry(entry, {'project': ROOT}) names = ('GetUint32', 'GetSize', 'GetString', 'GetSkip', 'GetStringRef', 'DoIgnore', 'DoServiceRequest', 'DoChannelWindowAdjust', 'DoUserAuthRequestEcc', 'DoUserAuthRequestEd25519') -# Verify this slice cannot accidentally change ordering or existing password logic. +# Parser edits must not change the independently applied ordering/password logic. from security_overrides import apply_edits, MODIFICATION_NOTICE, WOLFSSH_PARSER_EDITS -baseline = MODIFICATION_NOTICE + apply_edits(original.read_text(), entry.edits[len(WOLFSSH_PARSER_EDITS):]) +baseline = MODIFICATION_NOTICE + apply_edits(original.read_text(), tuple( + edit for edit in entry.edits if edit not in WOLFSSH_PARSER_EDITS)) for name in ('DoUserAuthRequestPassword', 'DoPacket', 'DoChannelFailure', 'ParseRSAPubKey', 'ParseECCPubKey', 'DoUserAuthRequestPublicKey'): assert extract(generated.decode(), name) == extract(baseline, name), name @@ -75,5 +76,5 @@ with tempfile.TemporaryDirectory(prefix='wolfssh-parser-') as directory: result = subprocess.run([str(binary)], capture_output=True, timeout=30) assert result.returncode != 0, f'Undetected mutation: {label}' print(f'PASS: {len(mutations)} parser guard-removal mutations rejected') -print('PASS: exact original hash; generated parser; unchanged ordering/password/deferred functions') +print('PASS: exact original hash; generated parser; parser-isolated ordering/password/deferred functions') print('NOTE: production build-tree registration/firmware not regenerated or validated') diff --git a/tools/security_overrides.py b/tools/security_overrides.py index 938580b..12c13c0 100644 --- a/tools/security_overrides.py +++ b/tools/security_overrides.py @@ -11,6 +11,7 @@ from __future__ import annotations import argparse from dataclasses import dataclass import hashlib +import json import os from pathlib import Path import re @@ -37,6 +38,7 @@ class Entry: sha256: str edits: tuple[Edit, ...] target: str = "" # Explicit nested library; empty means IDF COMPONENT_LIB. + header: bool = False # PUBLIC overlay, never a translation unit. MODIFICATION_NOTICE = """/* Modified by the ESP32_serial_swiss_army_knife project on 2026-09-15. @@ -47,6 +49,15 @@ MODIFICATION_NOTICE = """/* Modified by the ESP32_serial_swiss_army_knife projec """ +# Exact, reviewed consolidated delta; upstream mail patches are provenance only. +# No network, patch utility, fuzz, or managed-component mutation at configure time. +WOLFSSH_ORDER_DIR = Path(__file__).resolve().parent / "wolfssh_order" +WOLFSSH_ORDER_PLAN = json.loads((WOLFSSH_ORDER_DIR / "delta.json").read_text()) +WOLFSSH_ORDER_EDITS = { + path: tuple(Edit(**edit) for edit in item["edits"]) + for path, item in WOLFSSH_ORDER_PLAN.items() +} + WIPE = """/* Retired secret-bearing storage must not survive allocator reuse. */ static void security_override_wipe(void *buffer, size_t length) { @@ -427,7 +438,7 @@ ENTRIES = ( ), target="mbedx509"), Entry("wolfssh_internal", "wolfssl__wolfssh", "project", "managed_components/wolfssl__wolfssh/src/internal.c", - "81ff1f9166708abd5c2911e9fe57c0aee01c88b5d3f68c909ee8a856d37f36a9", WOLFSSH_PARSER_EDITS + ( + "81ff1f9166708abd5c2911e9fe57c0aee01c88b5d3f68c909ee8a856d37f36a9", WOLFSSH_ORDER_EDITS["src/internal.c"] + WOLFSSH_PARSER_EDITS + ( Edit(""" WS_UserAuthData_Password* pw = NULL; int ret = WS_SUCCESS; """, """ WS_UserAuthData_Password* pw = NULL; @@ -582,6 +593,18 @@ ENTRIES = ( ) +ENTRIES += ( + Entry("wolfssh_ssh", "wolfssl__wolfssh", "project", + "managed_components/wolfssl__wolfssh/src/ssh.c", + "a4f479ff87eea0980ec1ebdf2c7dd090da473780181b695a56799cb9611f4366", + WOLFSSH_ORDER_EDITS["src/ssh.c"]), + Entry("wolfssh_internal_header", "wolfssl__wolfssh", "project", + "managed_components/wolfssl__wolfssh/wolfssh/internal.h", + "8e417149a68f8a6c0506957adf014b3e6c1727a723536826ce5fb0c9e1f1aba3", + WOLFSSH_ORDER_EDITS["wolfssh/internal.h"], header=True), +) + + def apply_edits(text: str, edits: tuple[Edit, ...]) -> str: for index, edit in enumerate(edits, 1): count = text.count(edit.old) if edit.old else 0 @@ -607,6 +630,11 @@ def render_entry(entry: Entry, roots: dict[str, Path]) -> tuple[Path, bytes]: if entry.target and (entry.component != "mbedtls" or entry.target not in {"mbedtls", "mbedx509", "mbedcrypto"}): raise OverrideError("invalid nested target selection") + if entry.header and (entry.name != "wolfssh_internal_header" or + entry.component != "wolfssl__wolfssh" or entry.target or + entry.root != "project" or entry.source != + "managed_components/wolfssl__wolfssh/wolfssh/internal.h"): + raise OverrideError("unaudited header overlay") root = roots[entry.root].resolve() source = (root / entry.source).resolve() if not source.is_relative_to(root): @@ -615,7 +643,18 @@ def render_entry(entry: Entry, roots: dict[str, Path]) -> tuple[Path, bytes]: actual = hashlib.sha256(raw).hexdigest() if actual != entry.sha256: raise OverrideError(f"{entry.name}: SHA256 mismatch for {source}: expected {entry.sha256}, got {actual}; reaudit, do not repin blindly") - return source, (MODIFICATION_NOTICE + apply_edits(raw.decode("utf-8"), entry.edits)).encode("utf-8") + notice = MODIFICATION_NOTICE + if entry.component == "wolfssl__wolfssh": + notice += ("/* Ordering profile modified 2026-09-16: PR793/819/840/855/921\n" + " * plus project restricted no-EXT_INFO correction. Provenance and\n" + " * limitations: tools/wolfssh_order/README.md and delta.json.\n" + " */\n") + if entry.header: + notice += ("#if defined(_WOLFSSH_INTERNAL_H_) && \\\n" + " (!defined(SAK_WOLFSSH_ORDER_ABI) || SAK_WOLFSSH_ORDER_ABI != 20260916)\n" + '#error "Security override: stale wolfSSH internal.h included before overlay"\n' + "#endif\n#define SAK_WOLFSSH_ORDER_ABI 20260916\n") + return source, (notice + apply_edits(raw.decode("utf-8"), entry.edits)).encode("utf-8") def write_if_changed(path: Path, data: bytes) -> bool: @@ -659,10 +698,13 @@ def generate(idf: Path, project: Path, binary: Path, entries: tuple[Entry, ...] if source in seen: raise OverrideError(f"ambiguous duplicate source: {source}") seen.add(source) - target = output / entry.name / source.name + target = (output / "wolfssh_include" / "wolfssh" / source.name + if entry.header else output / entry.name / source.name) rendered.append((entry, source, target, data)) lines = ["# Generated by tools/security_overrides.py; do not edit.", - "set(SAK_SECURITY_OVERRIDE_IDS " + " ".join(names) + ")", + "set(SAK_SECURITY_OVERRIDE_IDS " + " ".join(e.name for e in entries if not e.header) + ")", + "set(SAK_SECURITY_HEADER_IDS " + " ".join(e.name for e in entries if e.header) + ")", + "set(SAK_SECURITY_WOLFSSH_INCLUDE " + cmake_quote(str(output / "wolfssh_include")) + ")", "set(SAK_SECURITY_VERSION_HEADER " + cmake_quote(str(version)) + ")"] for entry, source, target, _ in rendered: for key, value in (("COMPONENT", entry.component), ("TARGET", entry.target), ("ORIGINAL", str(source)), diff --git a/tools/wolfssh_order/793.patch b/tools/wolfssh_order/793.patch new file mode 100644 index 0000000..4caf6ea --- /dev/null +++ b/tools/wolfssh_order/793.patch @@ -0,0 +1,882 @@ +From 5fa6c0fce30a421879b355a007ef843eb48332d3 Mon Sep 17 00:00:00 2001 +From: JacobBarthelmeh +Date: Thu, 27 Mar 2025 08:54:23 -0600 +Subject: [PATCH 1/8] sanity checks on message types during rekey + +--- + src/internal.c | 55 +++++++++++++++++++++++++++++++++++++++++++--- + wolfssh/internal.h | 5 ++++- + 2 files changed, 56 insertions(+), 4 deletions(-) + +diff --git a/src/internal.c b/src/internal.c +index 63f0e1af7..739a9bad2 100644 +--- a/src/internal.c ++++ b/src/internal.c +@@ -595,6 +595,40 @@ static void HandshakeInfoFree(HandshakeInfo* hs, void* heap) + } + + ++/* RFC 4253 section 7.1, Once having sent SSH_MSG_KEXINIT the only messages ++* that can be sent are 1-19 (except SSH_MSG_SERVICE_REQUEST and ++* SSH_MSG_SERVICE_ACCEPT), 20-29 (except SSH_MSG_KEXINIT again), and 30-49 ++*/ ++INLINE static int IsMessageAllowedKeying(WOLFSSH *ssh, byte msg) ++{ ++ if (ssh->isKeying == 0) { ++ return 1; ++ } ++ ++ /* case of servie request or accept in 1-19 */ ++ if (msg == MSGID_SERVICE_REQUEST || msg == MSGID_SERVICE_ACCEPT) { ++ WLOG(WS_LOG_DEBUG, "Message ID %u not allowed by during rekeying", msg); ++ ssh->error = WS_REKEYING; ++ return 0; ++ } ++ ++ /* case of resending SSH_MSG_KEXINIT */ ++ if (msg == MSGID_KEXINIT) { ++ WLOG(WS_LOG_DEBUG, "Message ID %u not allowed by during rekeying", msg); ++ ssh->error = WS_REKEYING; ++ return 0; ++ } ++ ++ /* case where message id greater than 49 */ ++ if (msg >= MSGID_USERAUTH_REQUEST) { ++ WLOG(WS_LOG_DEBUG, "Message ID %u not allowed by during rekeying", msg); ++ ssh->error = WS_REKEYING; ++ return 0; ++ } ++ return 1; ++} ++ ++ + #ifndef NO_WOLFSSH_SERVER + INLINE static int IsMessageAllowedServer(WOLFSSH *ssh, byte msg) + { +@@ -673,8 +707,12 @@ INLINE static int IsMessageAllowedClient(WOLFSSH *ssh, byte msg) + #endif /* NO_WOLFSSH_CLIENT */ + + +-INLINE static int IsMessageAllowed(WOLFSSH *ssh, byte msg) ++INLINE static int IsMessageAllowed(WOLFSSH *ssh, byte msg, byte state) + { ++ if (state == WS_MSG_SEND && !IsMessageAllowedKeying(ssh, msg)) { ++ return 0; ++ } ++ + #ifndef NO_WOLFSSH_SERVER + if (ssh->ctx->side == WOLFSSH_ENDPOINT_SERVER) { + return IsMessageAllowedServer(ssh, msg); +@@ -5905,7 +5943,6 @@ static int DoNewKeys(WOLFSSH* ssh, byte* buf, word32 len, word32* idx) + HandshakeInfoFree(ssh->handshake, ssh->ctx->heap); + ssh->handshake = NULL; + WLOG(WS_LOG_DEBUG, "Keying completed"); +- + if (ssh->ctx->keyingCompletionCb) + ssh->ctx->keyingCompletionCb(ssh->keyingCompletionCtx); + } +@@ -9309,7 +9346,7 @@ static int DoPacket(WOLFSSH* ssh, byte* bufferConsumed) + return WS_OVERFLOW_E; + } + +- if (!IsMessageAllowed(ssh, msg)) { ++ if (!IsMessageAllowed(ssh, msg, WS_MSG_RECV)) { + return WS_MSGID_NOT_ALLOWED_E; + } + +@@ -15649,6 +15686,12 @@ int SendChannelEof(WOLFSSH* ssh, word32 peerChannelId) + if (ssh == NULL) + ret = WS_BAD_ARGUMENT; + ++ if (ret == WS_SUCCESS) { ++ if (!IsMessageAllowed(ssh, MSGID_CHANNEL_EOF, WS_MSG_SEND)) { ++ ret = WS_MSGID_NOT_ALLOWED_E; ++ } ++ } ++ + if (ret == WS_SUCCESS) { + channel = ChannelFind(ssh, peerChannelId, WS_CHANNEL_ID_PEER); + if (channel == NULL) +@@ -16077,6 +16120,12 @@ int SendChannelWindowAdjust(WOLFSSH* ssh, word32 channelId, + if (ssh == NULL) + ret = WS_BAD_ARGUMENT; + ++ if (ret == WS_SUCCESS) { ++ if (!IsMessageAllowed(ssh, MSGID_CHANNEL_WINDOW_ADJUST, WS_MSG_SEND)) { ++ ret = WS_MSGID_NOT_ALLOWED_E; ++ } ++ } ++ + channel = ChannelFind(ssh, channelId, WS_CHANNEL_ID_SELF); + if (channel == NULL) { + WLOG(WS_LOG_DEBUG, "Invalid channel"); +diff --git a/wolfssh/internal.h b/wolfssh/internal.h +index 261ae6d42..29a6f8ef8 100644 +--- a/wolfssh/internal.h ++++ b/wolfssh/internal.h +@@ -1249,6 +1249,10 @@ enum WS_MessageIds { + + #define CHANNEL_EXTENDED_DATA_STDERR WOLFSSH_EXT_DATA_STDERR + ++/* Used when checking IsMessageAllowed() to determine if createing and sending ++ * the message or receiving the message is allowed */ ++#define WS_MSG_SEND 1 ++#define WS_MSG_RECV 2 + + /* dynamic memory types */ + enum WS_DynamicTypes { +@@ -1442,4 +1446,3 @@ enum TerminalModes { + #endif + + #endif /* _WOLFSSH_INTERNAL_H_ */ +- + +From af45bc3719ddeac112d9d70b2e6a969f1aa3f3e7 Mon Sep 17 00:00:00 2001 +From: JacobBarthelmeh +Date: Tue, 13 May 2025 16:01:01 -0600 +Subject: [PATCH 2/8] update example client for rekey and sanity check on + window update after read attempt + +--- + examples/client/client.c | 11 +++++++++++ + src/ssh.c | 12 +++++++++++- + 2 files changed, 22 insertions(+), 1 deletion(-) + +diff --git a/examples/client/client.c b/examples/client/client.c +index e27305f72..f415bd801 100644 +--- a/examples/client/client.c ++++ b/examples/client/client.c +@@ -342,6 +342,9 @@ static THREAD_RET readInput(void* in) + ret = wolfSSH_stream_send(args->ssh, buf, sz); + wc_UnLockMutex(&args->lock); + if (ret <= 0) { ++ if (ret == WS_REKEYING) { ++ continue; ++ } + fprintf(stderr, "Couldn't send data\n"); + return THREAD_RET_SUCCESS; + } +@@ -472,8 +475,16 @@ static THREAD_RET readPeer(void* in) + continue; + } + #endif /* WOLFSSH_AGENT */ ++ else if (ret == WS_REKEYING) { ++ wolfSSH_worker(args->ssh, NULL); ++ ret = 0; ++ } + } + else if (ret != WS_EOF) { ++ if (ret == 0) { ++ bytes = 0; ++ continue; ++ } + err_sys("Stream read failed."); + } + } +diff --git a/src/ssh.c b/src/ssh.c +index 05c1a7b3e..2ed75d1bc 100644 +--- a/src/ssh.c ++++ b/src/ssh.c +@@ -1135,6 +1135,11 @@ int wolfSSH_stream_read(WOLFSSH* ssh, byte* buf, word32 bufSz) + return WS_ERROR; + } + ++ if (ssh->isKeying) { ++ ssh->error = WS_REKEYING; ++ return WS_FATAL_ERROR; ++ } ++ + inputBuffer = &ssh->channelList->inputBuffer; + ssh->error = WS_SUCCESS; + +@@ -1164,7 +1169,7 @@ int wolfSSH_stream_read(WOLFSSH* ssh, byte* buf, word32 bufSz) + } + + /* update internal input buffer based on data read */ +- if (ret == WS_SUCCESS) { ++ if (ret == WS_SUCCESS && !ssh->isKeying) { + int n; + + n = min(bufSz, inputBuffer->length - inputBuffer->idx); +@@ -2901,6 +2906,11 @@ int wolfSSH_ChannelRead(WOLFSSH_CHANNEL* channel, byte* buf, word32 bufSz) + if (channel == NULL || buf == NULL || bufSz == 0) + return WS_BAD_ARGUMENT; + ++ if (channel->ssh->isKeying) { ++ channel->ssh->error = WS_REKEYING; ++ return WS_REKEYING; ++ } ++ + bufSz = _ChannelRead(channel, buf, bufSz); + + WLOG(WS_LOG_DEBUG, "Leaving wolfSSH_ChannelRead(), bytesRxd = %d", + +From d74c942c84d44fb46d3a10cb56233b704733e466 Mon Sep 17 00:00:00 2001 +From: JacobBarthelmeh +Date: Thu, 15 May 2025 11:38:45 -0600 +Subject: [PATCH 3/8] refactor SFTP to use NoticeError + +--- + examples/sftpclient/sftpclient.c | 8 +++++++- + src/ssh.c | 2 +- + src/wolfsftp.c | 25 +++++++++++-------------- + 3 files changed, 19 insertions(+), 16 deletions(-) + +diff --git a/examples/sftpclient/sftpclient.c b/examples/sftpclient/sftpclient.c +index 779baff1f..a990bc14e 100644 +--- a/examples/sftpclient/sftpclient.c ++++ b/examples/sftpclient/sftpclient.c +@@ -1119,7 +1119,7 @@ static int doCmds(func_args* args) + /* alternate main loop for the autopilot get/receive */ + static int doAutopilot(int cmd, char* local, char* remote) + { +- int err; ++ int err = 0; + int ret = WS_SUCCESS; + char fullpath[128] = "."; + WS_SFTPNAME* name = NULL; +@@ -1156,6 +1156,12 @@ static int doAutopilot(int cmd, char* local, char* remote) + } + + do { ++ if (err == WS_REKEYING) { /* handle rekeying state */ ++ do { ++ ret = wolfSSH_worker(ssh, NULL); ++ } while (ret == WS_REKEYING); ++ } ++ + if (cmd == AUTOPILOT_PUT) { + ret = wolfSSH_SFTP_Put(ssh, local, fullpath, 0, NULL); + } +diff --git a/src/ssh.c b/src/ssh.c +index 2ed75d1bc..56c248589 100644 +--- a/src/ssh.c ++++ b/src/ssh.c +@@ -1201,7 +1201,7 @@ int wolfSSH_stream_send(WOLFSSH* ssh, byte* buf, word32 bufSz) + + if (ssh->isKeying) { + ssh->error = WS_REKEYING; +- return WS_REKEYING; ++ return WS_FATAL_ERROR; + } + + bytesTxd = SendChannelData(ssh, ssh->channelList->channel, buf, bufSz); +diff --git a/src/wolfsftp.c b/src/wolfsftp.c +index 761830a34..a95428fd0 100644 +--- a/src/wolfsftp.c ++++ b/src/wolfsftp.c +@@ -1418,7 +1418,11 @@ int wolfSSH_SFTP_read(WOLFSSH* ssh) + ret = wolfSSH_SFTP_buffer_read(ssh, &state->buffer, + state->buffer.sz); + if (ret < 0) { +- if (!NoticeError(ssh)) { ++ if (NoticeError(ssh)) { ++ /* keep state for returning to */ ++ ret = WS_FATAL_ERROR; ++ } ++ else { + wolfSSH_SFTP_ClearState(ssh, STATE_ID_RECV); + } + return ret; +@@ -7452,8 +7456,7 @@ int wolfSSH_SFTP_SendWritePacket(WOLFSSH* ssh, byte* handle, word32 handleSz, + /* send header and type specific data */ + ret = wolfSSH_SFTP_buffer_send(ssh, &state->buffer); + if (ret < 0) { +- if (ssh->error == WS_WANT_READ || +- ssh->error == WS_WANT_WRITE) { ++ if (NoticeError(ssh)) { + return WS_FATAL_ERROR; + } + state->state = STATE_SEND_WRITE_CLEANUP; +@@ -7465,12 +7468,8 @@ int wolfSSH_SFTP_SendWritePacket(WOLFSSH* ssh, byte* handle, word32 handleSz, + case STATE_SEND_WRITE_SEND_BODY: + WLOG(WS_LOG_SFTP, "SFTP SEND_WRITE STATE: SEND_BODY"); + state->sentSz = wolfSSH_stream_send(ssh, in, inSz); +- if (state->sentSz == WS_WINDOW_FULL || +- state->sentSz == WS_REKEYING || +- state->sentSz == WS_WANT_READ || +- state->sentSz == WS_WANT_WRITE) { +- ret = wolfSSH_worker(ssh, NULL); +- continue; /* skip past rest and send more */ ++ if (NoticeError(ssh)) { ++ return WS_FATAL_ERROR; + } + if (state->sentSz <= 0) { + ssh->error = state->sentSz; +@@ -7496,8 +7495,7 @@ int wolfSSH_SFTP_SendWritePacket(WOLFSSH* ssh, byte* handle, word32 handleSz, + state->maxSz = SFTP_GetHeader(ssh, &state->reqId, &type, + &state->buffer); + if (state->maxSz <= 0) { +- if (ssh->error == WS_WANT_READ || +- ssh->error == WS_WANT_WRITE) { ++ if (NoticeError(ssh)) { + return WS_FATAL_ERROR; + } + ssh->error = WS_SFTP_BAD_HEADER; +@@ -9167,10 +9165,9 @@ int wolfSSH_SFTP_Put(WOLFSSH* ssh, char* from, char* to, byte resume, + state->handle, state->handleSz, state->pOfst, + state->r, state->rSz); + if (sz <= 0) { +- if (ssh->error == WS_WANT_READ || +- ssh->error == WS_WANT_WRITE || +- ssh->error == WS_WINDOW_FULL) ++ if (NoticeError(ssh)) { + return WS_FATAL_ERROR; ++ } + } + else { + AddAssign64(state->pOfst, sz); + +From ff95f3c3029d766b114a91d98b013e0a1636a6c1 Mon Sep 17 00:00:00 2001 +From: JacobBarthelmeh +Date: Thu, 15 May 2025 13:32:33 -0600 +Subject: [PATCH 4/8] increase timeout time on test, fix spelling, add comment + on new arg + +--- + .github/workflows/sshd-test.yml | 2 +- + src/internal.c | 2 ++ + wolfssh/internal.h | 2 +- + 3 files changed, 4 insertions(+), 2 deletions(-) + +diff --git a/.github/workflows/sshd-test.yml b/.github/workflows/sshd-test.yml +index 3fbe3daf8..eb075a6f1 100644 +--- a/.github/workflows/sshd-test.yml ++++ b/.github/workflows/sshd-test.yml +@@ -66,7 +66,7 @@ jobs: + wolfssl: ${{ fromJson(needs.create_matrix.outputs['versions']) }} + name: Build and test wolfsshd + runs-on: ${{ matrix.os }} +- timeout-minutes: 10 ++ timeout-minutes: 15 + steps: + - name: Checking cache for wolfssl + uses: actions/cache@v4 +diff --git a/src/internal.c b/src/internal.c +index 739a9bad2..ff912ef74 100644 +--- a/src/internal.c ++++ b/src/internal.c +@@ -707,6 +707,8 @@ INLINE static int IsMessageAllowedClient(WOLFSSH *ssh, byte msg) + #endif /* NO_WOLFSSH_CLIENT */ + + ++/* 'state' argument is for if trying to send a message or receive one. ++ * Returns 1 if allowed 0 if not allowed. */ + INLINE static int IsMessageAllowed(WOLFSSH *ssh, byte msg, byte state) + { + if (state == WS_MSG_SEND && !IsMessageAllowedKeying(ssh, msg)) { +diff --git a/wolfssh/internal.h b/wolfssh/internal.h +index 29a6f8ef8..ad5e00b0e 100644 +--- a/wolfssh/internal.h ++++ b/wolfssh/internal.h +@@ -1249,7 +1249,7 @@ enum WS_MessageIds { + + #define CHANNEL_EXTENDED_DATA_STDERR WOLFSSH_EXT_DATA_STDERR + +-/* Used when checking IsMessageAllowed() to determine if createing and sending ++/* Used when checking IsMessageAllowed() to determine if creating and sending + * the message or receiving the message is allowed */ + #define WS_MSG_SEND 1 + #define WS_MSG_RECV 2 + +From 2a11471bb717a2ee6f06b3e1beab8a3e2b0ef261 Mon Sep 17 00:00:00 2001 +From: JacobBarthelmeh +Date: Tue, 16 Sep 2025 13:30:25 -0600 +Subject: [PATCH 5/8] refactor introducing more use of NoticeError + +--- + examples/echoserver/echoserver.c | 5 +- + examples/sftpclient/sftpclient.c | 81 ++++++++++++++++++++++++++++---- + src/wolfsftp.c | 35 +++++++------- + tests/api.c | 5 ++ + 4 files changed, 100 insertions(+), 26 deletions(-) + +diff --git a/examples/echoserver/echoserver.c b/examples/echoserver/echoserver.c +index 1fbd58a0c..8d14a7c95 100644 +--- a/examples/echoserver/echoserver.c ++++ b/examples/echoserver/echoserver.c +@@ -1416,8 +1416,11 @@ static int sftp_worker(thread_ctx_t* threadCtx) + } + else if (ret < 0) { + error = wolfSSH_get_error(ssh); +- if (error == WS_EOF) ++ if (error == WS_EOF) { ++ /* shutdown is happening, clear peek error */ ++ ret = 0; + break; ++ } + } + + if (ret == WS_FATAL_ERROR && error == 0) { +diff --git a/examples/sftpclient/sftpclient.c b/examples/sftpclient/sftpclient.c +index a990bc14e..b735c0d51 100644 +--- a/examples/sftpclient/sftpclient.c ++++ b/examples/sftpclient/sftpclient.c +@@ -566,11 +566,8 @@ static int doCmds(func_args* args) + } + + do { +- while (ret == WS_REKEYING || ssh->error == WS_REKEYING) { ++ while (wolfSSH_get_error(ssh) == WS_REKEYING) { + ret = wolfSSH_worker(ssh, NULL); +- if (ret != WS_SUCCESS && ret == WS_FATAL_ERROR) { +- ret = wolfSSH_get_error(ssh); +- } + } + + ret = wolfSSH_SFTP_Get(ssh, pt, to, resume, &myStatusCb); +@@ -747,6 +744,13 @@ static int doCmds(func_args* args) + + /* check directory is valid */ + do { ++ while (ret == WS_REKEYING || ssh->error == WS_REKEYING) { ++ ret = wolfSSH_worker(ssh, NULL); ++ if (ret != WS_SUCCESS && ret == WS_FATAL_ERROR) { ++ ret = wolfSSH_get_error(ssh); ++ } ++ } ++ + ret = wolfSSH_SFTP_STAT(ssh, pt, &atrb); + err = wolfSSH_get_error(ssh); + } while ((err == WS_WANT_READ || err == WS_WANT_WRITE) +@@ -828,6 +832,13 @@ static int doCmds(func_args* args) + + /* update permissions */ + do { ++ while (ret == WS_REKEYING || ssh->error == WS_REKEYING) { ++ ret = wolfSSH_worker(ssh, NULL); ++ if (ret != WS_SUCCESS && ret == WS_FATAL_ERROR) { ++ ret = wolfSSH_get_error(ssh); ++ } ++ } ++ + ret = wolfSSH_SFTP_CHMOD(ssh, pt, mode); + err = wolfSSH_get_error(ssh); + } while ((err == WS_WANT_READ || err == WS_WANT_WRITE) +@@ -878,6 +889,13 @@ static int doCmds(func_args* args) + } + + do { ++ while (ret == WS_REKEYING || ssh->error == WS_REKEYING) { ++ ret = wolfSSH_worker(ssh, NULL); ++ if (ret != WS_SUCCESS && ret == WS_FATAL_ERROR) { ++ ret = wolfSSH_get_error(ssh); ++ } ++ } ++ + ret = wolfSSH_SFTP_RMDIR(ssh, pt); + err = wolfSSH_get_error(ssh); + } while ((err == WS_WANT_READ || err == WS_WANT_WRITE) +@@ -924,6 +942,13 @@ static int doCmds(func_args* args) + } + + do { ++ while (ret == WS_REKEYING || ssh->error == WS_REKEYING) { ++ ret = wolfSSH_worker(ssh, NULL); ++ if (ret != WS_SUCCESS && ret == WS_FATAL_ERROR) { ++ ret = wolfSSH_get_error(ssh); ++ } ++ } ++ + ret = wolfSSH_SFTP_Remove(ssh, pt); + err = wolfSSH_get_error(ssh); + } while ((err == WS_WANT_READ || err == WS_WANT_WRITE) +@@ -1458,14 +1483,52 @@ THREAD_RETURN WOLFSSH_THREAD sftpclient_test(void* args) + + WFREE(workingDir, NULL, DYNAMIC_TYPE_TMP_BUFFER); + if (ret == WS_SUCCESS) { +- if (wolfSSH_shutdown(ssh) != WS_SUCCESS) { +- int rc; +- rc = wolfSSH_get_error(ssh); ++ int err; ++ ret = wolfSSH_shutdown(ssh); ++ ++ /* peer hung up, stop trying to shutdown */ ++ if (ret == WS_SOCKET_ERROR_E) { ++ ret = 0; ++ } ++ ++ err = wolfSSH_get_error(ssh); ++ if (err != WS_SOCKET_ERROR_E && ++ (err == WS_WANT_READ || err == WS_WANT_WRITE)) { ++ int maxAttempt = 10; /* make 10 attempts max before giving up */ ++ int attempt; ++ ++ for (attempt = 0; attempt < maxAttempt; attempt++) { ++ ret = wolfSSH_worker(ssh, NULL); ++ err = wolfSSH_get_error(ssh); ++ ++ /* peer succesfully closed down gracefully */ ++ if (ret == WS_CHANNEL_CLOSED) { ++ ret = 0; ++ break; ++ } + +- if (rc != WS_SOCKET_ERROR_E && rc != WS_EOF) +- printf("error with wolfSSH_shutdown()\n"); ++ /* peer hung up, stop shutdown */ ++ if (ret == WS_SOCKET_ERROR_E) { ++ ret = 0; ++ break; ++ } ++ ++ if (err == WS_WANT_READ || err == WS_WANT_WRITE) { ++ /* Wanting read or wanting write. Clear ret. */ ++ ret = 0; ++ } ++ else { ++ break; ++ } ++ } ++ ++ if (attempt == maxAttempt) { ++ printf("SFTP client gave up on gracefull shutdown," ++ "closing the socket\n"); ++ } + } + } ++ + WCLOSESOCKET(sockFd); + wolfSSH_free(ssh); + wolfSSH_CTX_free(ctx); +diff --git a/src/wolfsftp.c b/src/wolfsftp.c +index a95428fd0..53c5dfd1a 100644 +--- a/src/wolfsftp.c ++++ b/src/wolfsftp.c +@@ -865,6 +865,7 @@ static int SFTP_GetHeader(WOLFSSH* ssh, word32* reqId, byte* type, + */ + static int SFTP_SetHeader(WOLFSSH* ssh, word32 reqId, byte type, word32 len, + byte* buf) { ++ + c32toa(len + LENGTH_SZ + MSG_ID_SZ, buf); + buf[LENGTH_SZ] = type; + c32toa(reqId, buf + LENGTH_SZ + MSG_ID_SZ); +@@ -1170,8 +1171,9 @@ int wolfSSH_SFTP_accept(WOLFSSH* ssh) + case SFTP_EXT: + ret = SFTP_ServerRecvInit(ssh); + if (ret != WS_SUCCESS) { +- if (ssh->error != WS_WANT_READ && ssh->error != WS_WANT_WRITE) ++ if (!NoticeError(ssh)) { + wolfSSH_SFTP_ClearState(ssh, STATE_ID_ALL); ++ } + return ret; + } + ssh->sftpState = SFTP_RECV; +@@ -1573,8 +1575,9 @@ int wolfSSH_SFTP_read(WOLFSSH* ssh) + + /* break out if encountering an error with nothing stored to send */ + if (ret < 0 && !state->toSend) { +- if (ssh->error != WS_WANT_READ && ssh->error != WS_WANT_WRITE) ++ if (!NoticeError(ssh)) { + wolfSSH_SFTP_ClearState(ssh, STATE_ID_RECV); ++ } + return ret; + } + state->buffer.idx = 0; +@@ -7674,8 +7677,8 @@ int wolfSSH_SFTP_SendReadPacket(WOLFSSH* ssh, byte* handle, word32 handleSz, + /* send header and type specific data */ + ret = wolfSSH_SFTP_buffer_send(ssh, &state->buffer); + if (ret < 0) { +- if (ret == WS_REKEYING) { +- return ret; ++ if (NoticeError(ssh)) { ++ return WS_FATAL_ERROR; + } + if (ssh->error != WS_WANT_READ && + ssh->error != WS_WANT_WRITE) { +@@ -7693,14 +7696,12 @@ int wolfSSH_SFTP_SendReadPacket(WOLFSSH* ssh, byte* handle, word32 handleSz, + /* Get response */ + if ((ret = SFTP_GetHeader(ssh, &state->reqId, &state->type, + &state->buffer)) <= 0) { +- if (ssh->error != WS_WANT_READ && +- ssh->error != WS_WANT_WRITE) { ++ if (!NoticeError(ssh)) { + state->state = STATE_SEND_READ_CLEANUP; + continue; + } + return WS_FATAL_ERROR; + } +- + ret = wolfSSH_SFTP_buffer_create(ssh, &state->buffer, ret); + if (ret != WS_SUCCESS) { + state->state = STATE_SEND_READ_CLEANUP; +@@ -7718,8 +7719,9 @@ int wolfSSH_SFTP_SendReadPacket(WOLFSSH* ssh, byte* handle, word32 handleSz, + state->state = STATE_SEND_READ_CLEANUP; + continue; + } +- else ++ else { + ssh->reqId++; ++ } + + if (state->type == WOLFSSH_FTP_DATA) + state->state = STATE_SEND_READ_FTP_DATA; +@@ -7737,8 +7739,7 @@ int wolfSSH_SFTP_SendReadPacket(WOLFSSH* ssh, byte* handle, word32 handleSz, + /* get size of string and place it into out buffer */ + ret = wolfSSH_stream_read(ssh, szFlat, UINT32_SZ); + if (ret < 0) { +- if (ssh->error != WS_WANT_READ && +- ssh->error != WS_WANT_WRITE) { ++ if (!NoticeError(ssh)) { + state->state = STATE_SEND_READ_CLEANUP; + continue; + } +@@ -7917,8 +7918,9 @@ int wolfSSH_SFTP_MKDIR(WOLFSSH* ssh, char* dir, WS_SFTP_FILEATRB* atr) + /* send header and type specific data */ + ret = wolfSSH_SFTP_buffer_send(ssh, &state->buffer); + if (ret < 0) { +- if (ssh->error != WS_WANT_READ && ssh->error != WS_WANT_WRITE) ++ if (!NoticeError(ssh)) { + wolfSSH_SFTP_ClearState(ssh, STATE_ID_MKDIR); ++ } + return ret; + } + +@@ -7931,8 +7933,9 @@ int wolfSSH_SFTP_MKDIR(WOLFSSH* ssh, char* dir, WS_SFTP_FILEATRB* atr) + /* Get response */ + if ((ret = SFTP_GetHeader(ssh, &state->reqId, &type, + &state->buffer)) <= 0) { +- if (ssh->error != WS_WANT_READ && ssh->error != WS_WANT_WRITE) ++ if (!NoticeError(ssh)) { + wolfSSH_SFTP_ClearState(ssh, STATE_ID_MKDIR); ++ } + return WS_FATAL_ERROR; + } + +@@ -7963,8 +7966,9 @@ int wolfSSH_SFTP_MKDIR(WOLFSSH* ssh, char* dir, WS_SFTP_FILEATRB* atr) + ret = wolfSSH_SFTP_buffer_read(ssh, &state->buffer, + wolfSSH_SFTP_buffer_size(&state->buffer)); + if (ret < 0) { +- if (ssh->error != WS_WANT_READ && ssh->error != WS_WANT_WRITE) +- wolfSSH_SFTP_ClearState(ssh, STATE_ID_MKDIR); ++ if (!NoticeError(ssh)) { ++ wolfSSH_SFTP_ClearState(ssh, STATE_ID_MKDIR); ++ } + return WS_FATAL_ERROR; + } + +@@ -8031,8 +8035,7 @@ WS_SFTPNAME* wolfSSH_SFTP_ReadDir(WOLFSSH* ssh, byte* handle, + case STATE_READDIR_NAME: + name = wolfSSH_SFTP_DoName(ssh); + if (name == NULL) { +- if (ssh->error != WS_WANT_READ +- && ssh->error != WS_WANT_WRITE) { ++ if (!NoticeError(ssh)) { + wolfSSH_SFTP_ClearState(ssh, STATE_ID_READDIR); + } + return NULL; +diff --git a/tests/api.c b/tests/api.c +index 2bef34998..701425d20 100644 +--- a/tests/api.c ++++ b/tests/api.c +@@ -1075,6 +1075,11 @@ static void test_wolfSSH_SFTP_SendReadPacket(void) + } + } + ++ /* take care of re-keying state before shutdown call */ ++ while (wolfSSH_get_error(ssh) == WS_REKEYING) { ++ wolfSSH_worker(ssh, NULL); ++ } ++ + argsCount = wolfSSH_shutdown(ssh); + if (argsCount == WS_SOCKET_ERROR_E) { + /* If the socket is closed on shutdown, peer is gone, this is OK. */ + +From 813ec263cc56e7c9093135d854b3fc887633d368 Mon Sep 17 00:00:00 2001 +From: JacobBarthelmeh +Date: Tue, 16 Sep 2025 13:52:12 -0600 +Subject: [PATCH 6/8] fix for scan-build report of unused return value + +--- + examples/sftpclient/sftpclient.c | 5 ++++- + 1 file changed, 4 insertions(+), 1 deletion(-) + +diff --git a/examples/sftpclient/sftpclient.c b/examples/sftpclient/sftpclient.c +index b735c0d51..1194b0de5 100644 +--- a/examples/sftpclient/sftpclient.c ++++ b/examples/sftpclient/sftpclient.c +@@ -566,8 +566,11 @@ static int doCmds(func_args* args) + } + + do { +- while (wolfSSH_get_error(ssh) == WS_REKEYING) { ++ while (ret == WS_REKEYING || ssh->error == WS_REKEYING) { + ret = wolfSSH_worker(ssh, NULL); ++ if (ret != WS_SUCCESS && ret == WS_FATAL_ERROR) { ++ ret = wolfSSH_get_error(ssh); ++ } + } + + ret = wolfSSH_SFTP_Get(ssh, pt, to, resume, &myStatusCb); + +From cc17941a6125daefbadb8c236fdeaeb1a21ec786 Mon Sep 17 00:00:00 2001 +From: JacobBarthelmeh +Date: Tue, 16 Sep 2025 14:16:16 -0600 +Subject: [PATCH 7/8] adjust test case to account for re-keying return + +--- + tests/api.c | 8 +++++--- + 1 file changed, 5 insertions(+), 3 deletions(-) + +diff --git a/tests/api.c b/tests/api.c +index 701425d20..9da02849b 100644 +--- a/tests/api.c ++++ b/tests/api.c +@@ -1057,14 +1057,16 @@ static void test_wolfSSH_SFTP_SendReadPacket(void) + outSz = WOLFSSH_MAX_SFTP_RW / 2; + rxSz = wolfSSH_SFTP_SendReadPacket(ssh, handle, handleSz, + ofst, out, outSz); +- AssertIntGT(rxSz, 0); +- AssertIntLE(rxSz, outSz); ++ if (wolfSSH_get_error(ssh) != WS_REKEYING) { ++ AssertIntGT(rxSz, 0); ++ AssertIntLE(rxSz, outSz); ++ } + + /* read all */ + outSz = WOLFSSH_MAX_SFTP_RW; + rxSz = wolfSSH_SFTP_SendReadPacket(ssh, handle, handleSz, + ofst, out, outSz); +- if (rxSz != WS_REKEYING) { ++ if (wolfSSH_get_error(ssh) != WS_REKEYING) { + AssertIntGT(rxSz, 0); + AssertIntLE(rxSz, outSz); + } + +From 4862400a374253216e596ff5c3b018b857015cbb Mon Sep 17 00:00:00 2001 +From: JacobBarthelmeh +Date: Mon, 6 Oct 2025 00:42:05 -0600 +Subject: [PATCH 8/8] fix spelling issues and SFTP send state + +--- + .github/workflows/sshd-test.yml | 2 +- + examples/client/client.c | 4 ++-- + examples/sftpclient/sftpclient.c | 7 ++++--- + src/internal.c | 2 +- + src/wolfsftp.c | 15 +++++++++------ + 5 files changed, 17 insertions(+), 13 deletions(-) + +diff --git a/.github/workflows/sshd-test.yml b/.github/workflows/sshd-test.yml +index eb075a6f1..3fbe3daf8 100644 +--- a/.github/workflows/sshd-test.yml ++++ b/.github/workflows/sshd-test.yml +@@ -66,7 +66,7 @@ jobs: + wolfssl: ${{ fromJson(needs.create_matrix.outputs['versions']) }} + name: Build and test wolfsshd + runs-on: ${{ matrix.os }} +- timeout-minutes: 15 ++ timeout-minutes: 10 + steps: + - name: Checking cache for wolfssl + uses: actions/cache@v4 +diff --git a/examples/client/client.c b/examples/client/client.c +index f415bd801..49d00f44f 100644 +--- a/examples/client/client.c ++++ b/examples/client/client.c +@@ -481,10 +481,10 @@ static THREAD_RET readPeer(void* in) + } + } + else if (ret != WS_EOF) { +- if (ret == 0) { ++ if (ret == 0) { + bytes = 0; + continue; +- } ++ } + err_sys("Stream read failed."); + } + } +diff --git a/examples/sftpclient/sftpclient.c b/examples/sftpclient/sftpclient.c +index 1194b0de5..e074b3d13 100644 +--- a/examples/sftpclient/sftpclient.c ++++ b/examples/sftpclient/sftpclient.c +@@ -1184,7 +1184,7 @@ static int doAutopilot(int cmd, char* local, char* remote) + } + + do { +- if (err == WS_REKEYING) { /* handle rekeying state */ ++ if (err == WS_REKEYING || err == WS_WINDOW_FULL) { /* handle rekeying state */ + do { + ret = wolfSSH_worker(ssh, NULL); + } while (ret == WS_REKEYING); +@@ -1198,7 +1198,8 @@ static int doAutopilot(int cmd, char* local, char* remote) + } + err = wolfSSH_get_error(ssh); + } while ((err == WS_WANT_READ || err == WS_WANT_WRITE || +- err == WS_CHAN_RXD || err == WS_REKEYING) && ++ err == WS_CHAN_RXD || err == WS_REKEYING || ++ err == WS_WINDOW_FULL) && + ret == WS_FATAL_ERROR); + + if (ret != WS_SUCCESS) { +@@ -1504,7 +1505,7 @@ THREAD_RETURN WOLFSSH_THREAD sftpclient_test(void* args) + ret = wolfSSH_worker(ssh, NULL); + err = wolfSSH_get_error(ssh); + +- /* peer succesfully closed down gracefully */ ++ /* peer successfully closed down gracefully */ + if (ret == WS_CHANNEL_CLOSED) { + ret = 0; + break; +diff --git a/src/internal.c b/src/internal.c +index ff912ef74..b9e3a3432 100644 +--- a/src/internal.c ++++ b/src/internal.c +@@ -605,7 +605,7 @@ INLINE static int IsMessageAllowedKeying(WOLFSSH *ssh, byte msg) + return 1; + } + +- /* case of servie request or accept in 1-19 */ ++ /* case of service request or accept in 1-19 */ + if (msg == MSGID_SERVICE_REQUEST || msg == MSGID_SERVICE_ACCEPT) { + WLOG(WS_LOG_DEBUG, "Message ID %u not allowed by during rekeying", msg); + ssh->error = WS_REKEYING; +diff --git a/src/wolfsftp.c b/src/wolfsftp.c +index 53c5dfd1a..998806aef 100644 +--- a/src/wolfsftp.c ++++ b/src/wolfsftp.c +@@ -416,6 +416,7 @@ static INLINE int NoticeError(WOLFSSH* ssh) + return (ssh->error == WS_WANT_READ || + ssh->error == WS_WANT_WRITE || + ssh->error == WS_CHAN_RXD || ++ ssh->error == WS_WINDOW_FULL || + ssh->error == WS_REKEYING); + } + +@@ -865,7 +866,6 @@ static int SFTP_GetHeader(WOLFSSH* ssh, word32* reqId, byte* type, + */ + static int SFTP_SetHeader(WOLFSSH* ssh, word32 reqId, byte type, word32 len, + byte* buf) { +- + c32toa(len + LENGTH_SZ + MSG_ID_SZ, buf); + buf[LENGTH_SZ] = type; + c32toa(reqId, buf + LENGTH_SZ + MSG_ID_SZ); +@@ -7471,12 +7471,15 @@ int wolfSSH_SFTP_SendWritePacket(WOLFSSH* ssh, byte* handle, word32 handleSz, + case STATE_SEND_WRITE_SEND_BODY: + WLOG(WS_LOG_SFTP, "SFTP SEND_WRITE STATE: SEND_BODY"); + state->sentSz = wolfSSH_stream_send(ssh, in, inSz); +- if (NoticeError(ssh)) { +- return WS_FATAL_ERROR; +- } + if (state->sentSz <= 0) { +- ssh->error = state->sentSz; + ret = WS_FATAL_ERROR; ++ if (NoticeError(ssh)) { ++ ret = wolfSSH_worker(ssh,NULL); ++ continue; ++ } ++ ++ /* if it was not a notice error then clean up the state and ++ * exit out */ + state->state = STATE_SEND_WRITE_CLEANUP; + continue; + } +@@ -9170,7 +9173,7 @@ int wolfSSH_SFTP_Put(WOLFSSH* ssh, char* from, char* to, byte resume, + if (sz <= 0) { + if (NoticeError(ssh)) { + return WS_FATAL_ERROR; +- } ++ } + } + else { + AddAssign64(state->pOfst, sz); diff --git a/tools/wolfssh_order/819.patch b/tools/wolfssh_order/819.patch new file mode 100644 index 0000000..deea0b3 --- /dev/null +++ b/tools/wolfssh_order/819.patch @@ -0,0 +1,46 @@ +From 201029797b260eee894b12d488bce6022290bf67 Mon Sep 17 00:00:00 2001 +From: JacobBarthelmeh +Date: Fri, 18 Jul 2025 15:43:26 -0600 +Subject: [PATCH] only send ext info once after SSH_MSG_NEWKEYS + +--- + src/internal.c | 6 +++++- + wolfssh/internal.h | 1 + + 2 files changed, 6 insertions(+), 1 deletion(-) + +diff --git a/src/internal.c b/src/internal.c +index 080ded6f3..912315ca2 100644 +--- a/src/internal.c ++++ b/src/internal.c +@@ -4080,7 +4080,7 @@ static int DoKexInit(WOLFSSH* ssh, byte* buf, word32 len, word32* idx) + if (ret == WS_SUCCESS) { + /* Only checking for this is we are server. Our client does + * not have anything to say to a server, yet. */ +- if (side == WOLFSSH_ENDPOINT_SERVER) { ++ if (side == WOLFSSH_ENDPOINT_SERVER && !ssh->extInfoSent) { + byte extInfo; + + /* Match the client accepts extInfo. */ +@@ -13216,6 +13216,10 @@ int SendExtInfo(WOLFSSH* ssh) + } + + if (ret == WS_SUCCESS) { ++ ssh->sendExtInfo = 0; ++ ssh->extInfoSent = 1; /* RFC 8308 section 2.4 ext. info should only be ++ * sent after SSH_MSG_NEWKEYS or after ++ * SSH_MSG_USERAUTH_SUCCESS. Not on re-key */ + ret = wolfSSH_SendPacket(ssh); + } + +diff --git a/wolfssh/internal.h b/wolfssh/internal.h +index 1b60139a1..26c3a05b0 100644 +--- a/wolfssh/internal.h ++++ b/wolfssh/internal.h +@@ -844,6 +844,7 @@ struct WOLFSSH { + byte sendTerminalRequest; + byte userAuthPkDone; + byte sendExtInfo; ++ byte extInfoSent; /* track if the ext info has already been sent */ + byte* peerSigId; + word32 peerSigIdSz; + diff --git a/tools/wolfssh_order/840.patch b/tools/wolfssh_order/840.patch new file mode 100644 index 0000000..5b2d18a --- /dev/null +++ b/tools/wolfssh_order/840.patch @@ -0,0 +1,147 @@ +From 9dc1071da7e560db2ea899fa23aab885a25ea862 Mon Sep 17 00:00:00 2001 +From: JacobBarthelmeh +Date: Mon, 13 Oct 2025 09:44:29 -0600 +Subject: [PATCH 1/2] improvements to keying and track side + +--- + src/internal.c | 35 ++++++++++++++++++++++++++++++----- + wolfssh/internal.h | 5 +++++ + 2 files changed, 35 insertions(+), 5 deletions(-) + +diff --git a/src/internal.c b/src/internal.c +index edab14eb4..230904489 100644 +--- a/src/internal.c ++++ b/src/internal.c +@@ -1096,7 +1096,7 @@ WOLFSSH* SshInit(WOLFSSH* ssh, WOLFSSH_CTX* ctx) + ssh->fs = NULL; + ssh->acceptState = ACCEPT_BEGIN; + ssh->clientState = CLIENT_BEGIN; +- ssh->isKeying = 1; ++ ssh->isKeying = 0; /* initial state of not keying yet */ + ssh->authId = ID_USERAUTH_PUBLICKEY; + ssh->supportedAuth[0] = ID_USERAUTH_PUBLICKEY; + ssh->supportedAuth[1] = ID_USERAUTH_PASSWORD; +@@ -4058,6 +4058,15 @@ static int DoKexInit(WOLFSSH* ssh, byte* buf, word32 len, word32* idx) + ret = WS_BAD_ARGUMENT; + } + ++ if (ret == WS_SUCCESS) { ++ /* Check if already in process of keying and error out if so. */ ++ if (ssh->isKeying & WOLFSSH_PEER_IS_KEYING) { ++ WLOG(WS_LOG_ERROR, ++ "Already in keying process and got KEX init"); ++ ret = WS_INVALID_STATE_E; ++ } ++ } ++ + /* + * I don't need to save what the client sends here. I should decode + * each list into a local array of IDs, and pick the one the peer is +@@ -4067,6 +4076,8 @@ static int DoKexInit(WOLFSSH* ssh, byte* buf, word32 len, word32* idx) + */ + + if (ret == WS_SUCCESS) { ++ /* Set peer is keying flag after receiving SSH_MSG_KEX_INIT */ ++ ssh->isKeying |= WOLFSSH_PEER_IS_KEYING; + if (ssh->handshake == NULL) { + ssh->handshake = HandshakeInfoNew(ssh->ctx->heap); + if (ssh->handshake == NULL) { +@@ -5881,6 +5892,13 @@ static int DoNewKeys(WOLFSSH* ssh, byte* buf, word32 len, word32* idx) + if (ssh == NULL || ssh->handshake == NULL) + ret = WS_BAD_ARGUMENT; + ++ if (ret == WS_SUCCESS) { ++ if (ssh->isKeying & WOLFSSH_SELF_IS_KEYING) { ++ WLOG(WS_LOG_ERROR, "Keying failed"); ++ ret = WS_INVALID_STATE_E; ++ } ++ } ++ + if (ret == WS_SUCCESS) { + ssh->peerEncryptId = ssh->handshake->encryptId; + ssh->peerMacId = ssh->handshake->macId; +@@ -5941,7 +5959,9 @@ static int DoNewKeys(WOLFSSH* ssh, byte* buf, word32 len, word32* idx) + if (ret == WS_SUCCESS) { + ssh->rxCount = 0; + ssh->highwaterFlag = 0; +- ssh->isKeying = 0; ++ ++ /* Clear peer is keying flag */ ++ ssh->isKeying &= ~WOLFSSH_PEER_IS_KEYING; + HandshakeInfoFree(ssh->handshake, ssh->ctx->heap); + ssh->handshake = NULL; + WLOG(WS_LOG_DEBUG, "Keying completed"); +@@ -9405,7 +9425,7 @@ static int DoPacket(WOLFSSH* ssh, byte* bufferConsumed) + case MSGID_KEXINIT: + WLOG(WS_LOG_DEBUG, "Decoding MSGID_KEXINIT"); + ret = DoKexInit(ssh, buf + idx, payloadSz, &payloadIdx); +- if (ssh->isKeying == 1 && ++ if (ssh->isKeying && + ssh->connectState == CONNECT_SERVER_CHANNEL_REQUEST_DONE) { + if (ssh->handshake->kexId == ID_DH_GEX_SHA256) { + #if !defined(WOLFSSH_NO_DH) && !defined(WOLFSSH_NO_DH_GEX_SHA256) +@@ -10501,7 +10521,8 @@ int SendKexInit(WOLFSSH* ssh) + } + + if (ret == WS_SUCCESS) { +- ssh->isKeying = 1; ++ /* Set self is keying flag since we started sending the KEX init msg */ ++ ssh->isKeying |= WOLFSSH_SELF_IS_KEYING; + if (ssh->handshake == NULL) { + ssh->handshake = HandshakeInfoNew(ssh->ctx->heap); + if (ssh->handshake == NULL) { +@@ -12534,9 +12555,13 @@ int SendNewKeys(WOLFSSH* ssh) + ssh->txCount = 0; + } + +- if (ret == WS_SUCCESS) ++ if (ret == WS_SUCCESS) { + ret = wolfSSH_SendPacket(ssh); + ++ /* Clear self is keying flag */ ++ ssh->isKeying &= ~WOLFSSH_SELF_IS_KEYING; ++ } ++ + WLOG(WS_LOG_DEBUG, "Leaving SendNewKeys(), ret = %d", ret); + return ret; + } +diff --git a/wolfssh/internal.h b/wolfssh/internal.h +index 1b7dada16..6df5f1147 100644 +--- a/wolfssh/internal.h ++++ b/wolfssh/internal.h +@@ -473,6 +473,11 @@ enum NameIdType { + #define WOLFSSH_KEY_QUANTITY_REQ 1 + #endif + ++/* Keep track of keying state for both sides of the connection. ++ * WOLFSSH_SELF_IS_KEYING gets set on sending KEX init and ++ * WOLFSSH_PEER_IS_KEYING gets set on receiving KEX init */ ++#define WOLFSSH_PEER_IS_KEYING 0x01 ++#define WOLFSSH_SELF_IS_KEYING 0x02 + + WOLFSSH_LOCAL byte NameToId(const char* name, word32 nameSz); + WOLFSSH_LOCAL const char* IdToName(byte id); + +From 024b14124aa2434e90468408b94121c897311f37 Mon Sep 17 00:00:00 2001 +From: JacobBarthelmeh +Date: Mon, 13 Oct 2025 22:47:45 -0600 +Subject: [PATCH 2/2] update Kex Init response after adding keying track sides + +--- + src/internal.c | 3 ++- + 1 file changed, 2 insertions(+), 1 deletion(-) + +diff --git a/src/internal.c b/src/internal.c +index 230904489..db70d0f6f 100644 +--- a/src/internal.c ++++ b/src/internal.c +@@ -4338,7 +4338,8 @@ static int DoKexInit(WOLFSSH* ssh, byte* buf, word32 len, word32* idx) + byte scratchLen[LENGTH_SZ]; + word32 strSz = 0; + +- if (!ssh->isKeying) { ++ /* respond with KEX Init message if not having initiated the keying */ ++ if ((ssh->isKeying & WOLFSSH_SELF_IS_KEYING) == 0) { + WLOG(WS_LOG_DEBUG, "Keying initiated"); + ret = SendKexInit(ssh); + } diff --git a/tools/wolfssh_order/855.patch b/tools/wolfssh_order/855.patch new file mode 100644 index 0000000..bd9a34d --- /dev/null +++ b/tools/wolfssh_order/855.patch @@ -0,0 +1,1709 @@ +From 73b165651c80a20047d913d32b0ffac47cb99ef6 Mon Sep 17 00:00:00 2001 +From: John Safranek +Date: Mon, 17 Nov 2025 09:50:48 -0800 +Subject: [PATCH 1/7] Client Out Of Order Messaging Checking + +1. Add macro for logging an expected message. +2. Add an expected message ID to the HandshakeInfo. +3. Add a message ID for "none (0)". +4. Add a check in IsMessageAllowedClient() for the expected message ID. + Clear it if successful. +5. The KEXDH messages sent to the server have expected responses. Set + them if sending the message is successful. +6. Add the set of message ID ranges and macros for testing if a message + ID is in a specific range. +7. Add flags for having sent the kexinit message and received it. Tweak + the checks for isKeying and these flags. +8. IsMessageAllowedClient() to check for appropriate messages at the + appropriate time during the connect. +--- + src/internal.c | 158 +++++++++++++++++++++++++++++++++++---------- + wolfssh/internal.h | 64 ++++++++++++++---- + wolfssh/log.h | 2 + + 3 files changed, 178 insertions(+), 46 deletions(-) + +diff --git a/src/internal.c b/src/internal.c +index 5c553c6b7..3b4fe5f30 100644 +--- a/src/internal.c ++++ b/src/internal.c +@@ -612,8 +612,8 @@ INLINE static int IsMessageAllowedKeying(WOLFSSH *ssh, byte msg) + return 0; + } + +- /* case of resending SSH_MSG_KEXINIT */ +- if (msg == MSGID_KEXINIT) { ++ /* case of peer resending SSH_MSG_KEXINIT */ ++ if ((ssh->isKeying & WOLFSSH_PEER_IS_KEYING) && msg == MSGID_KEXINIT) { + WLOG(WS_LOG_DEBUG, "Message ID %u not allowed by during rekeying", msg); + ssh->error = WS_REKEYING; + return 0; +@@ -632,34 +632,50 @@ INLINE static int IsMessageAllowedKeying(WOLFSSH *ssh, byte msg) + #ifndef NO_WOLFSSH_SERVER + INLINE static int IsMessageAllowedServer(WOLFSSH *ssh, byte msg) + { ++ /* Only the server should send these messages, never receive. */ ++ if (msg == MSGID_SERVICE_ACCEPT) { ++ WLOG(WS_LOG_DEBUG, "Message ID %u not allowed by %s %s", ++ msg, "client", "ever"); ++ return 0; ++ } ++ ++ /* Transport Layer Generic messages are always allowed. */ ++ if (MSGIDLIMIT_TRANS_GEN(msg)) { ++ return 1; ++ } ++ + /* Has client userauth started? */ ++ /* Allows the server to receive up to KEXDH GEX Request during KEX. */ + if (ssh->acceptState < ACCEPT_KEYED) { +- if (msg > MSGID_KEXDH_LIMIT) { ++ if (msg > MSGID_KEXDH_GEX_REQUEST) { + return 0; + } + } + /* Is server userauth complete? */ + if (ssh->acceptState < ACCEPT_SERVER_USERAUTH_SENT) { ++ /* The server should only receive the user auth request message, ++ * it should not accept the other user auth messages, it sends ++ * them. (>50) */ + /* Explicitly check for messages not allowed before user + * authentication has comleted. */ +- if (msg >= MSGID_USERAUTH_LIMIT) { +- WLOG(WS_LOG_DEBUG, "Message ID %u not allowed by server " +- "before user authentication is complete", msg); ++ if (MSGIDLIMIT_POST_USERAUTH(msg)) { ++ WLOG(WS_LOG_DEBUG, "Message ID %u not allowed by %s %s", ++ msg, "server", "before user authentication is complete"); + return 0; + } + /* Explicitly check for the user authentication messages that + * only the server sends, it shouldn't receive them. */ +- if ((msg > MSGID_USERAUTH_RESTRICT) && ++ if ((msg > MSGID_USERAUTH_REQUEST) && + (msg != MSGID_USERAUTH_INFO_RESPONSE)) { +- WLOG(WS_LOG_DEBUG, "Message ID %u not allowed by server " +- "during user authentication", msg); ++ WLOG(WS_LOG_DEBUG, "Message ID %u not allowed by %s %s", ++ msg, "server", "during user authentication"); + return 0; + } + } + else { +- if (msg >= MSGID_USERAUTH_RESTRICT && msg < MSGID_USERAUTH_LIMIT) { +- WLOG(WS_LOG_DEBUG, "Message ID %u not allowed by server " +- "after user authentication", msg); ++ if (msg >= MSGID_USERAUTH_REQUEST && msg < MSGID_GLOBAL_REQUEST) { ++ WLOG(WS_LOG_DEBUG, "Message ID %u not allowed by %s %s", ++ msg, "server", "after user authentication"); + return 0; + } + } +@@ -672,37 +688,95 @@ INLINE static int IsMessageAllowedServer(WOLFSSH *ssh, byte msg) + #ifndef NO_WOLFSSH_CLIENT + INLINE static int IsMessageAllowedClient(WOLFSSH *ssh, byte msg) + { +- /* Has client userauth started? */ +- if (ssh->connectState < CONNECT_CLIENT_KEXDH_INIT_SENT) { +- if (msg >= MSGID_KEXDH_LIMIT) { ++ /* Only the client should send these messages, never receive. */ ++ if (msg == MSGID_SERVICE_REQUEST || msg == MSGID_USERAUTH_REQUEST) { ++ WLOG(WS_LOG_DEBUG, "Message ID %u not allowed by %s %s", ++ msg, "client", "ever"); ++ return 0; ++ } ++ ++ if (msg == MSGID_SERVICE_ACCEPT) { ++ if (ssh->connectState == CONNECT_CLIENT_USERAUTH_REQUEST_SENT) { ++ return 1; ++ } ++ else { ++ WLOG(WS_LOG_DEBUG, "Message ID %u not allowed by %s %s", ++ msg, "client", "after starting user auth"); + return 0; + } + } +- /* Is client userauth complete? */ +- if (ssh->connectState < CONNECT_SERVER_USERAUTH_ACCEPT_DONE) { +- /* Explicitly check for messages not allowed before user +- * authentication has comleted. */ +- if (msg >= MSGID_USERAUTH_LIMIT) { +- WLOG(WS_LOG_DEBUG, "Message ID %u not allowed by client " +- "before user authentication is complete", msg); ++ ++ /* Transport Layer Generic messages are always allowed. */ ++ if (MSGIDLIMIT_TRANS_GEN(msg)) { ++ return 1; ++ } ++ ++ /* Is KEX complete? */ ++ if (MSGIDLIMIT_TRANS(msg)) { ++ if (ssh->isKeying & WOLFSSH_PEER_IS_KEYING) { ++ /* MSGID_KEXINIT not allowed when keying. */ ++ if (msg == MSGID_KEXINIT) { ++ WLOG(WS_LOG_DEBUG, "Message ID %u not allowed by %s %s", ++ msg, "client", "when keying"); ++ return 0; ++ } ++ ++ /* Error if expecting a specific message and didn't receive. */ ++ if (ssh->handshake && ssh->handshake->expectMsgId != MSGID_NONE) { ++ if (msg != ssh->handshake->expectMsgId) { ++ WLOG(WS_LOG_DEBUG, ++ "Message ID %u not the expected message %u", ++ msg, ssh->handshake->expectMsgId); ++ return 0; ++ } ++ else { ++ /* Got the expected message, clear expectation. */ ++ ssh->handshake->expectMsgId = MSGID_NONE; ++ return 1; ++ } ++ } ++ } ++ else { ++ /* MSGID_KEXINIT only allowed when not keying. */ ++ if (msg == MSGID_KEXINIT) { ++ return 1; ++ } ++ ++ /* All other transport KEX and ALGO messages are not allowed ++ * when not keying. */ ++ WLOG(WS_LOG_DEBUG, "Message ID %u not allowed by %s %s", ++ msg, "client", "when not keying"); + return 0; + } +- /* Explicitly check for the user authentication message that +- * only the client sends, it shouldn't receive it. */ +- if (msg == MSGID_USERAUTH_RESTRICT) { +- WLOG(WS_LOG_DEBUG, "Message ID %u not allowed by client " +- "during user authentication", msg); ++ } ++ ++ /* Is client userauth complete? */ ++ if (ssh->connectState >= CONNECT_KEYED ++ && ssh->connectState < CONNECT_SERVER_USERAUTH_ACCEPT_DONE) { ++ /* The endpoints should not allow message IDs greater than or ++ * equal to msgid 80 before user authentication is complete. ++ * Per RFC 4252 section 6. */ ++ if (MSGIDLIMIT_POST_USERAUTH(msg)) { ++ WLOG(WS_LOG_DEBUG, "Message ID %u not allowed by %s %s", ++ msg, "client", "before user authentication is complete"); + return 0; + } ++ else if (MSGIDLIMIT_AUTH(msg)) { ++ return 1; ++ } + } + else { +- if (msg >= MSGID_USERAUTH_RESTRICT && msg < MSGID_USERAUTH_LIMIT) { +- WLOG(WS_LOG_DEBUG, "Message ID %u not allowed by client " +- "after user authentication", msg); ++ if (MSGIDLIMIT_POST_USERAUTH(msg)) { ++ return 1; ++ } ++ else if (MSGIDLIMIT_AUTH(msg)) { ++ WLOG(WS_LOG_DEBUG, "Message ID %u not allowed by %s %s", ++ msg, "client", "after user authentication"); + return 0; + } + } +- return 1; ++ ++ return 0; + } + #endif /* NO_WOLFSSH_CLIENT */ + +@@ -711,7 +785,7 @@ INLINE static int IsMessageAllowedClient(WOLFSSH *ssh, byte msg) + * Returns 1 if allowed 0 if not allowed. */ + INLINE static int IsMessageAllowed(WOLFSSH *ssh, byte msg, byte state) + { +- if (state == WS_MSG_SEND && !IsMessageAllowedKeying(ssh, msg)) { ++ if (!IsMessageAllowedKeying(ssh, msg)) { + return 0; + } + +@@ -725,6 +799,7 @@ INLINE static int IsMessageAllowed(WOLFSSH *ssh, byte msg, byte state) + return IsMessageAllowedClient(ssh, msg); + } + #endif /* NO_WOLFSSH_CLIENT */ ++ (void)state; + return 0; + } + +@@ -5872,8 +5947,10 @@ static int DoKexDhReply(WOLFSSH* ssh, byte* buf, word32 len, word32* idx) + ret = GenerateKeys(ssh, hashId, !ssh->handshake->useEccMlKem); + } + +- if (ret == WS_SUCCESS) ++ if (ret == WS_SUCCESS) { + ret = SendNewKeys(ssh); ++ ssh->handshake->expectMsgId = MSGID_NEWKEYS; ++ } + + if (sigKeyBlock_ptr) + WFREE(sigKeyBlock_ptr, ssh->ctx->heap, DYNTYPE_PRIVKEY); +@@ -10648,8 +10725,9 @@ int SendKexInit(WOLFSSH* ssh) + ret = BundlePacket(ssh); + } + +- if (ret == WS_SUCCESS) ++ if (ret == WS_SUCCESS) { + ret = wolfSSH_SendPacket(ssh); ++ } + + if (ret != WS_WANT_WRITE && ret != WS_SUCCESS) + PurgePacket(ssh); +@@ -12613,6 +12691,11 @@ int SendKexDhGexRequest(WOLFSSH* ssh) + if (ret == WS_SUCCESS) + ret = wolfSSH_SendPacket(ssh); + ++ if (ret == WS_SUCCESS) { ++ WLOG_EXPECT_MSGID(MSGID_KEXDH_GEX_GROUP); ++ ssh->handshake->expectMsgId = MSGID_KEXDH_GEX_GROUP; ++ } ++ + WLOG(WS_LOG_DEBUG, "Leaving SendKexDhGexRequest(), ret = %d", ret); + return ret; + } +@@ -12701,6 +12784,7 @@ int SendKexDhInit(WOLFSSH* ssh) + #endif + int ret = WS_SUCCESS; + byte msgId = MSGID_KEXDH_INIT; ++ byte expectMsgId = MSGID_KEXDH_REPLY; + byte e[MAX_KEX_KEY_SZ+1]; /* plus 1 in case of padding. */ + word32 eSz = (word32)sizeof(e); + byte ePad = 0; +@@ -12752,6 +12836,7 @@ int SendKexDhInit(WOLFSSH* ssh) + generator = ssh->handshake->generator; + generatorSz = ssh->handshake->generatorSz; + msgId = MSGID_KEXDH_GEX_INIT; ++ expectMsgId = MSGID_KEXDH_GEX_REPLY; + break; + #endif + #ifndef WOLFSSH_NO_ECDH_SHA2_NISTP256 +@@ -12963,6 +13048,11 @@ int SendKexDhInit(WOLFSSH* ssh) + if (ret == WS_SUCCESS) + ret = wolfSSH_SendPacket(ssh); + ++ if (ret == WS_SUCCESS) { ++ WLOG_EXPECT_MSGID(expectMsgId); ++ ssh->handshake->expectMsgId = expectMsgId; ++ } ++ + WLOG(WS_LOG_DEBUG, "Leaving SendKexDhInit(), ret = %d", ret); + return ret; + } +diff --git a/wolfssh/internal.h b/wolfssh/internal.h +index 6df5f1147..9bba76367 100644 +--- a/wolfssh/internal.h ++++ b/wolfssh/internal.h +@@ -607,6 +607,7 @@ typedef struct Keys { + + + typedef struct HandshakeInfo { ++ byte expectMsgId; + byte kexId; + byte kexIdGuess; + byte kexHashId; +@@ -1183,6 +1184,8 @@ enum ProcessReplyStates { + + + enum WS_MessageIds { ++ MSGID_NONE = 0, ++ + MSGID_DISCONNECT = 1, + MSGID_IGNORE = 2, + MSGID_UNIMPLEMENTED = 3, +@@ -1238,19 +1241,56 @@ enum WS_MessageIds { + }; + + +-/* Allows the server to receive up to KEXDH GEX Request during KEX. */ +-#define MSGID_KEXDH_LIMIT MSGID_KEXDH_GEX_REQUEST +- +-/* The endpoints should not allow message IDs greater than or +- * equal to msgid 80 before user authentication is complete. +- * Per RFC 4252 section 6. */ +-#define MSGID_USERAUTH_LIMIT 80 ++/* The following message ID ranges are described in RFC 5251, section 7. */ ++enum WS_MessageIdLimits { ++/* Transport Layer Protocol: */ ++ MSGIDLIMIT_TRANS_MIN = 1, ++ MSGIDLIMIT_TRANS_GEN_MIN = 1, ++ MSGIDLIMIT_TRANS_GEN_MAX = 19, ++ MSGIDLIMIT_TRANS_ALGO_MIN = 20, ++ MSGIDLIMIT_TRANS_ALGO_MAX = 29, ++ MSGIDLIMIT_TRANS_KEX_MIN = 30, ++ MSGIDLIMIT_TRANS_KEX_MAX = 49, ++ MSGIDLIMIT_TRANS_MAX = 49, ++/* User Authentication Protocol: */ ++ MSGIDLIMIT_AUTH_MIN = 50, ++ MSGIDLIMIT_AUTH_GEN_MIN = 50, ++ MSGIDLIMIT_AUTH_GEN_MAX = 59, ++ MSGIDLIMIT_AUTH_METH_MIN = 60, ++ MSGIDLIMIT_AUTH_METH_MAX = 79, ++ MSGIDLIMIT_AUTH_MAX = 79, ++/* Connection Protocol: */ ++ MSGIDLIMIT_CONN_MIN = 80, ++ MSGIDLIMIT_CONN_GEN_MIN = 80, ++ MSGIDLIMIT_CONN_GEN_MAX = 89, ++ MSGIDLIMIT_CONN_CHAN_MIN = 90, ++ MSGIDLIMIT_CONN_CHAN_MAX = 127, ++ MSGIDLIMIT_CONN_MAX = 127, ++/* Reserved For Client Protocols: */ ++ MSGIDLIMIT_RESERVED_MIN = 128, ++ MSGIDLIMIT_RESERVED_MAX = 191, ++/* Local Extensions: */ ++ MSGIDLIMIT_EXTENDED_MIN = 192, ++ MSGIDLIMIT_EXTENDED_MAX = 255, ++}; + +-/* The client should only send the user auth request message +- * (50), it should not accept it. The server should only receive +- * the user auth request message, it should not accept the other +- * user auth messages, it sends them. (>50) */ +-#define MSGID_USERAUTH_RESTRICT 50 ++/* Message ID bounds checking. */ ++#define MSGIDLIMIT_BOUND(x,y,z) ((x) >= (y) && (x) <= (z)) ++#define MSGIDLIMIT_COMP(x,name) \ ++ MSGIDLIMIT_BOUND((x),MSGIDLIMIT_##name##_MIN,MSGIDLIMIT_##name##_MAX) ++#define MSGIDLIMIT_TRANS(x) MSGIDLIMIT_COMP((x),TRANS) ++#define MSGIDLIMIT_TRANS_GEN(x) MSGIDLIMIT_COMP((x),TRANS_GEN) ++#define MSGIDLIMIT_TRANS_ALGO(x) MSGIDLIMIT_COMP((x),TRANS_ALGO) ++#define MSGIDLIMIT_TRANS_KEX(x) MSGIDLIMIT_COMP((x),TRANS_KEX) ++#define MSGIDLIMIT_AUTH(x) MSGIDLIMIT_COMP((x),AUTH) ++#define MSGIDLIMIT_AUTH_GEN(x) MSGIDLIMIT_COMP((x),AUTH_GEN) ++#define MSGIDLIMIT_AUTH_METH(x) MSGIDLIMIT_COMP((x),AUTH_METH) ++#define MSGIDLIMIT_CONN(x) MSGIDLIMIT_COMP((x),CONN) ++#define MSGIDLIMIT_CONN_GEN(x) MSGIDLIMIT_COMP((x),CONN_GEN) ++#define MSGIDLIMIT_CONN_CHAN(x) MSGIDLIMIT_COMP((x),CONN_CHAN) ++#define MSGIDLIMIT_RESERVED(x) MSGIDLIMIT_COMP((x),RESERVED) ++#define MSGIDLIMIT_EXTENDED(x) MSGIDLIMIT_COMP((x),EXTENDED) ++#define MSGIDLIMIT_POST_USERAUTH(x) ((x) >= MSGIDLIMIT_CONN_MIN) + + + #define CHANNEL_EXTENDED_DATA_STDERR WOLFSSH_EXT_DATA_STDERR +diff --git a/wolfssh/log.h b/wolfssh/log.h +index 8758e7d43..89bd93827 100644 +--- a/wolfssh/log.h ++++ b/wolfssh/log.h +@@ -81,6 +81,8 @@ WOLFSSH_API void wolfSSH_Log(enum wolfSSH_LogLevel, + #define WLOG(...) WC_DO_NOTHING + #endif + ++#define WLOG_EXPECT_MSGID(x) WLOG(WS_LOG_DEBUG, "Expecting message %d", (x)) ++ + #ifdef __cplusplus + } + #endif + +From a87ab400b3900d1e7fdda33c898094d6e3ada21d Mon Sep 17 00:00:00 2001 +From: John Safranek +Date: Mon, 24 Nov 2025 09:15:29 -0800 +Subject: [PATCH 2/7] Guard out IsKeyingAllowed() as it is stepping on the + wrong messages. + +--- + src/internal.c | 4 ++++ + 1 file changed, 4 insertions(+) + +diff --git a/src/internal.c b/src/internal.c +index 3b4fe5f30..0a7a2ad00 100644 +--- a/src/internal.c ++++ b/src/internal.c +@@ -595,6 +595,7 @@ static void HandshakeInfoFree(HandshakeInfo* hs, void* heap) + } + + ++#if 0 + /* RFC 4253 section 7.1, Once having sent SSH_MSG_KEXINIT the only messages + * that can be sent are 1-19 (except SSH_MSG_SERVICE_REQUEST and + * SSH_MSG_SERVICE_ACCEPT), 20-29 (except SSH_MSG_KEXINIT again), and 30-49 +@@ -627,6 +628,7 @@ INLINE static int IsMessageAllowedKeying(WOLFSSH *ssh, byte msg) + } + return 1; + } ++#endif + + + #ifndef NO_WOLFSSH_SERVER +@@ -785,9 +787,11 @@ INLINE static int IsMessageAllowedClient(WOLFSSH *ssh, byte msg) + * Returns 1 if allowed 0 if not allowed. */ + INLINE static int IsMessageAllowed(WOLFSSH *ssh, byte msg, byte state) + { ++#if 0 + if (!IsMessageAllowedKeying(ssh, msg)) { + return 0; + } ++#endif + + #ifndef NO_WOLFSSH_SERVER + if (ssh->ctx->side == WOLFSSH_ENDPOINT_SERVER) { + +From 5ae5c250e2d9b3bfdaea4f9c73cdf4b9f2592daa Mon Sep 17 00:00:00 2001 +From: Andrew Hutchings +Date: Mon, 1 Dec 2025 15:03:53 +0000 +Subject: [PATCH 3/7] Add tests and fix issues + +--- + Makefile.am | 1 + + src/include.am | 12 ++ + src/internal.c | 62 +++---- + tests/include.am | 8 +- + tests/regress.c | 401 +++++++++++++++++++++++++++++++++++++++++++++ + wolfssh/internal.h | 5 + + 6 files changed, 446 insertions(+), 43 deletions(-) + create mode 100644 tests/regress.c + +diff --git a/Makefile.am b/Makefile.am +index e729d5150..d5db195d5 100644 +--- a/Makefile.am ++++ b/Makefile.am +@@ -2,6 +2,7 @@ + bin_PROGRAMS = + noinst_HEADERS = + lib_LTLIBRARIES = ++noinst_LTLIBRARIES = + noinst_PROGRAMS = + nobase_include_HEADERS = + check_PROGRAMS = +diff --git a/src/include.am b/src/include.am +index 8b70bdccd..88e535610 100644 +--- a/src/include.am ++++ b/src/include.am +@@ -11,30 +11,42 @@ src_libwolfssh_la_SOURCES = src/ssh.c \ + src_libwolfssh_la_CPPFLAGS = -DBUILDING_WOLFSSH ${AM_CPPFLAGS} + src_libwolfssh_la_LDFLAGS = -no-undefined -version-info ${WOLFSSH_LIBRARY_VERSION} + ++noinst_LTLIBRARIES += src/libwolfssh_test.la ++src_libwolfssh_test_la_SOURCES = $(src_libwolfssh_la_SOURCES) ++src_libwolfssh_test_la_CPPFLAGS = -DBUILDING_WOLFSSH -DWOLFSSH_TEST_INTERNAL ${AM_CPPFLAGS} ++src_libwolfssh_test_la_LDFLAGS = -no-undefined ++ + if !BUILD_INLINE + src_libwolfssh_la_SOURCES += src/misc.c ++src_libwolfssh_test_la_SOURCES += src/misc.c + endif + + if BUILD_KEYGEN + src_libwolfssh_la_SOURCES += src/keygen.c ++src_libwolfssh_test_la_SOURCES += src/keygen.c + endif + + if BUILD_SCP + src_libwolfssh_la_SOURCES += src/wolfscp.c ++src_libwolfssh_test_la_SOURCES += src/wolfscp.c + endif + + if BUILD_SFTP + src_libwolfssh_la_SOURCES += src/wolfsftp.c ++src_libwolfssh_test_la_SOURCES += src/wolfsftp.c + endif + + if BUILD_TERM + src_libwolfssh_la_SOURCES += src/wolfterm.c ++src_libwolfssh_test_la_SOURCES += src/wolfterm.c + endif + + if BUILD_AGENT + src_libwolfssh_la_SOURCES += src/agent.c ++src_libwolfssh_test_la_SOURCES += src/agent.c + endif + + if BUILD_CERTS + src_libwolfssh_la_SOURCES += src/certman.c ++src_libwolfssh_test_la_SOURCES += src/certman.c + endif +diff --git a/src/internal.c b/src/internal.c +index 0a7a2ad00..dfadcedea 100644 +--- a/src/internal.c ++++ b/src/internal.c +@@ -595,42 +595,6 @@ static void HandshakeInfoFree(HandshakeInfo* hs, void* heap) + } + + +-#if 0 +-/* RFC 4253 section 7.1, Once having sent SSH_MSG_KEXINIT the only messages +-* that can be sent are 1-19 (except SSH_MSG_SERVICE_REQUEST and +-* SSH_MSG_SERVICE_ACCEPT), 20-29 (except SSH_MSG_KEXINIT again), and 30-49 +-*/ +-INLINE static int IsMessageAllowedKeying(WOLFSSH *ssh, byte msg) +-{ +- if (ssh->isKeying == 0) { +- return 1; +- } +- +- /* case of service request or accept in 1-19 */ +- if (msg == MSGID_SERVICE_REQUEST || msg == MSGID_SERVICE_ACCEPT) { +- WLOG(WS_LOG_DEBUG, "Message ID %u not allowed by during rekeying", msg); +- ssh->error = WS_REKEYING; +- return 0; +- } +- +- /* case of peer resending SSH_MSG_KEXINIT */ +- if ((ssh->isKeying & WOLFSSH_PEER_IS_KEYING) && msg == MSGID_KEXINIT) { +- WLOG(WS_LOG_DEBUG, "Message ID %u not allowed by during rekeying", msg); +- ssh->error = WS_REKEYING; +- return 0; +- } +- +- /* case where message id greater than 49 */ +- if (msg >= MSGID_USERAUTH_REQUEST) { +- WLOG(WS_LOG_DEBUG, "Message ID %u not allowed by during rekeying", msg); +- ssh->error = WS_REKEYING; +- return 0; +- } +- return 1; +-} +-#endif +- +- + #ifndef NO_WOLFSSH_SERVER + INLINE static int IsMessageAllowedServer(WOLFSSH *ssh, byte msg) + { +@@ -694,6 +658,7 @@ INLINE static int IsMessageAllowedClient(WOLFSSH *ssh, byte msg) + if (msg == MSGID_SERVICE_REQUEST || msg == MSGID_USERAUTH_REQUEST) { + WLOG(WS_LOG_DEBUG, "Message ID %u not allowed by %s %s", + msg, "client", "ever"); ++ ssh->error = WS_MSGID_NOT_ALLOWED_E; + return 0; + } + +@@ -720,6 +685,7 @@ INLINE static int IsMessageAllowedClient(WOLFSSH *ssh, byte msg) + if (msg == MSGID_KEXINIT) { + WLOG(WS_LOG_DEBUG, "Message ID %u not allowed by %s %s", + msg, "client", "when keying"); ++ ssh->error = WS_REKEYING; + return 0; + } + +@@ -729,6 +695,7 @@ INLINE static int IsMessageAllowedClient(WOLFSSH *ssh, byte msg) + WLOG(WS_LOG_DEBUG, + "Message ID %u not the expected message %u", + msg, ssh->handshake->expectMsgId); ++ ssh->error = WS_REKEYING; + return 0; + } + else { +@@ -748,6 +715,7 @@ INLINE static int IsMessageAllowedClient(WOLFSSH *ssh, byte msg) + * when not keying. */ + WLOG(WS_LOG_DEBUG, "Message ID %u not allowed by %s %s", + msg, "client", "when not keying"); ++ ssh->error = WS_MSGID_NOT_ALLOWED_E; + return 0; + } + } +@@ -761,9 +729,17 @@ INLINE static int IsMessageAllowedClient(WOLFSSH *ssh, byte msg) + if (MSGIDLIMIT_POST_USERAUTH(msg)) { + WLOG(WS_LOG_DEBUG, "Message ID %u not allowed by %s %s", + msg, "client", "before user authentication is complete"); ++ ssh->error = WS_MSGID_NOT_ALLOWED_E; + return 0; + } + else if (MSGIDLIMIT_AUTH(msg)) { ++ /* Do not accept any userauth messages until we've asked for auth. */ ++ if (ssh->connectState < CONNECT_CLIENT_USERAUTH_REQUEST_SENT) { ++ WLOG(WS_LOG_DEBUG, "Message ID %u not allowed by %s %s", ++ msg, "client", "before sending userauth request"); ++ ssh->error = WS_MSGID_NOT_ALLOWED_E; ++ return 0; ++ } + return 1; + } + } +@@ -774,6 +750,7 @@ INLINE static int IsMessageAllowedClient(WOLFSSH *ssh, byte msg) + else if (MSGIDLIMIT_AUTH(msg)) { + WLOG(WS_LOG_DEBUG, "Message ID %u not allowed by %s %s", + msg, "client", "after user authentication"); ++ ssh->error = WS_MSGID_NOT_ALLOWED_E; + return 0; + } + } +@@ -787,12 +764,6 @@ INLINE static int IsMessageAllowedClient(WOLFSSH *ssh, byte msg) + * Returns 1 if allowed 0 if not allowed. */ + INLINE static int IsMessageAllowed(WOLFSSH *ssh, byte msg, byte state) + { +-#if 0 +- if (!IsMessageAllowedKeying(ssh, msg)) { +- return 0; +- } +-#endif +- + #ifndef NO_WOLFSSH_SERVER + if (ssh->ctx->side == WOLFSSH_ENDPOINT_SERVER) { + return IsMessageAllowedServer(ssh, msg); +@@ -807,6 +778,13 @@ INLINE static int IsMessageAllowed(WOLFSSH *ssh, byte msg, byte state) + return 0; + } + ++#ifdef WOLFSSH_TEST_INTERNAL ++int wolfSSH_TestIsMessageAllowed(WOLFSSH* ssh, byte msg, byte state) ++{ ++ return IsMessageAllowed(ssh, msg, state); ++} ++#endif ++ + + static const char cannedKexAlgoNames[] = + #if !defined(WOLFSSH_NO_NISTP256_MLKEM768_SHA256) +diff --git a/tests/include.am b/tests/include.am +index f14b3007a..a9aed41c0 100644 +--- a/tests/include.am ++++ b/tests/include.am +@@ -3,7 +3,8 @@ + # All paths should be given relative to the root + + check_PROGRAMS += tests/unit.test tests/api.test \ +- tests/testsuite.test tests/kex.test ++ tests/testsuite.test tests/kex.test \ ++ tests/regress.test + + tests_unit_test_SOURCES = tests/unit.c tests/unit.h + tests_unit_test_CPPFLAGS = -DNO_MAIN_DRIVER $(AM_CPPFLAGS) +@@ -43,3 +44,8 @@ tests_kex_test_SOURCES = tests/kex.c tests/kex.h \ + tests_kex_test_CPPFLAGS = -DNO_MAIN_DRIVER $(AM_CPPFLAGS) + tests_kex_test_LDADD = src/libwolfssh.la + tests_kex_test_DEPENDENCIES = src/libwolfssh.la ++ ++tests_regress_test_SOURCES = tests/regress.c ++tests_regress_test_CPPFLAGS = -DNO_MAIN_DRIVER -DWOLFSSH_TEST_INTERNAL $(AM_CPPFLAGS) ++tests_regress_test_LDADD = src/libwolfssh_test.la ++tests_regress_test_DEPENDENCIES = src/libwolfssh_test.la +diff --git a/tests/regress.c b/tests/regress.c +new file mode 100644 +index 000000000..fbd6032e5 +--- /dev/null ++++ b/tests/regress.c +@@ -0,0 +1,401 @@ ++/* regress.c ++ * ++ * Regression coverage for message ordering / keying state handling. ++ * ++ * Copyright (C) 2025 wolfSSL Inc. ++ */ ++ ++#ifdef HAVE_CONFIG_H ++ #include ++#endif ++ ++#ifdef WOLFSSL_USER_SETTINGS ++ #include ++#else ++ #include ++#endif ++ ++#include ++#include ++#include ++#include ++ ++#include ++#include ++#include ++ ++#ifndef WOLFSSH_NO_ABORT ++ #define WABORT() abort() ++#else ++ #define WABORT() ++#endif ++ ++#define PrintError(description, result) do { \ ++ printf("\nERROR - %s line %d failed with:", __FILE__, __LINE__); \ ++ printf("\n expected: "); printf description; \ ++ printf("\n result: "); printf result; printf("\n\n"); \ ++} while(0) ++ ++#define Fail(description, result) do { \ ++ PrintError(description, result); \ ++ WABORT(); \ ++} while(0) ++ ++#define Assert(test, description, result) if (!(test)) Fail(description, result) ++ ++#define AssertTrue(x) Assert((x), ("%s is true", #x), (#x " => FALSE")) ++#define AssertFalse(x) Assert(!(x), ("%s is false", #x), (#x " => TRUE")) ++#define AssertNotNull(x) Assert((x), ("%s is not null", #x), (#x " => NULL")) ++#define AssertIntEQ(x, y) do { int _x = (int)(x); int _y = (int)(y); \ ++ Assert(_x == _y, ("%s == %s", #x, #y), ("%d != %d", _x, _y)); } while (0) ++ ++ ++static void ResetSession(WOLFSSH* ssh) ++{ ++ if (ssh->handshake != NULL) { ++ WFREE(ssh->handshake, ssh->ctx->heap, DYNTYPE_HS); ++ ssh->handshake = NULL; ++ } ++ ssh->isKeying = 0; ++ ssh->connectState = CONNECT_BEGIN; ++ ssh->error = 0; ++} ++ ++ ++static HandshakeInfo* AllocHandshake(WOLFSSH* ssh) ++{ ++ HandshakeInfo* hs; ++ ++ hs = (HandshakeInfo*)WMALLOC(sizeof(HandshakeInfo), ssh->ctx->heap, ++ DYNTYPE_HS); ++ AssertNotNull(hs); ++ WMEMSET(hs, 0, sizeof(HandshakeInfo)); ++ hs->blockSz = MIN_BLOCK_SZ; ++ hs->eSz = (word32)sizeof(hs->e); ++ hs->xSz = (word32)sizeof(hs->x); ++ ++ return hs; ++} ++ ++/* Build a minimal SSH binary packet carrying only a message ID. ++ * Layout: uint32 packetLen, byte padLen, payload[msgId], pad[padLen]. ++ * Choose padLen so total is 8-byte aligned for the clear transport case. */ ++static word32 BuildPacket(byte msgId, byte* out, word32 outSz) ++{ ++ byte padLen = 6; /* 1 (msgId) +1 (padLen) +6 = 8 */ ++ word32 packetLen = 1 + 1 + padLen; /* payload + padLen field + pad */ ++ word32 need = 4 + packetLen; ++ ++ AssertTrue(outSz >= need); ++ out[0] = (byte)(packetLen >> 24); ++ out[1] = (byte)(packetLen >> 16); ++ out[2] = (byte)(packetLen >> 8); ++ out[3] = (byte)(packetLen); ++ out[4] = padLen; ++ out[5] = msgId; ++ WMEMSET(out + 6, 0, padLen); ++ return need; ++} ++ ++static byte ParseMsgId(const byte* pkt, word32 sz) ++{ ++ AssertTrue(sz >= 6); ++ return pkt[5]; ++} ++ ++/* Simple in-memory transport harness */ ++typedef struct { ++ byte* in; /* data to feed into client */ ++ word32 inSz; ++ word32 inOff; ++ byte* out; /* data written by client */ ++ word32 outSz; ++ word32 outCap; ++} MemIo; ++ ++/* Minimal send/recv helpers for future transport-level tests; keep them static ++ * and unused for now to avoid warnings when Werror is on. */ ++#ifdef WOLFSSH_TEST_MEMIO ++static int MemRecv(WOLFSSH* ssh, void* buf, word32 sz, void* ctx) ++{ ++ (void)ssh; ++ MemIo* io = (MemIo*)ctx; ++ word32 remain = io->inSz - io->inOff; ++ if (remain == 0) ++ return WS_CBIO_ERR_WANT_READ; ++ if (sz > remain) ++ sz = remain; ++ WMEMCPY(buf, io->in + io->inOff, sz); ++ io->inOff += sz; ++ return (int)sz; ++} ++ ++static int MemSend(WOLFSSH* ssh, void* buf, word32 sz, void* ctx) ++{ ++ (void)ssh; ++ MemIo* io = (MemIo*)ctx; ++ if (io->outSz + sz > io->outCap) { ++ return WS_CBIO_ERR_GENERAL; ++ } ++ WMEMCPY(io->out + io->outSz, buf, sz); ++ io->outSz += sz; ++ return (int)sz; ++} ++ ++static void MemIoInit(MemIo* io, byte* in, word32 inSz, byte* out, word32 outCap) ++{ ++ io->in = in; ++ io->inSz = inSz; ++ io->inOff = 0; ++ io->out = out; ++ io->outSz = 0; ++ io->outCap = outCap; ++} ++#endif ++ ++ ++/* Reject auth messages while the peer is still keying and the client ++ * expects the KEX reply. */ ++static void TestAuthMessageBlockedDuringKeying(WOLFSSH* ssh) ++{ ++ int allowed; ++ ++ ResetSession(ssh); ++ ssh->isKeying = WOLFSSH_PEER_IS_KEYING; ++ ssh->connectState = CONNECT_CLIENT_KEXDH_INIT_SENT; ++ ssh->handshake = AllocHandshake(ssh); ++ ssh->handshake->expectMsgId = MSGID_KEXDH_REPLY; ++ ++ allowed = wolfSSH_TestIsMessageAllowed(ssh, MSGID_USERAUTH_FAILURE, ++ WS_MSG_RECV); ++ AssertFalse(allowed); ++ ++ /* The expected message must be allowed and clear the expectation. */ ++ allowed = wolfSSH_TestIsMessageAllowed(ssh, MSGID_KEXDH_REPLY, ++ WS_MSG_RECV); ++ AssertTrue(allowed); ++ AssertIntEQ(ssh->handshake->expectMsgId, MSGID_NONE); ++} ++ ++/* Reject USERAUTH_FAILURE with password list during keying (password-leak PoC). */ ++static void TestUserauthFailureDuringKeying(WOLFSSH* ssh) ++{ ++ byte buf[32]; ++ word32 sz; ++ int allowed; ++ ++ ResetSession(ssh); ++ ssh->isKeying = WOLFSSH_PEER_IS_KEYING; ++ ssh->connectState = CONNECT_CLIENT_KEXDH_INIT_SENT; ++ ssh->handshake = AllocHandshake(ssh); ++ ssh->handshake->expectMsgId = MSGID_KEXDH_REPLY; ++ ++ sz = BuildPacket(MSGID_USERAUTH_FAILURE, buf, sizeof(buf)); ++ allowed = wolfSSH_TestIsMessageAllowed(ssh, ParseMsgId(buf, sz), ++ WS_MSG_RECV); ++ AssertFalse(allowed); ++} ++ ++ ++/* Expect an abort/error to be set when password-leak sequence hits during keying. */ ++static void TestPasswordLeakAborts(WOLFSSH* ssh) ++{ ++ byte buf[32]; ++ word32 sz; ++ int allowed; ++ ++ ResetSession(ssh); ++ ssh->isKeying = WOLFSSH_PEER_IS_KEYING; ++ ssh->connectState = CONNECT_CLIENT_KEXDH_INIT_SENT; ++ ssh->handshake = AllocHandshake(ssh); ++ ssh->handshake->expectMsgId = MSGID_KEXDH_REPLY; ++ ++ sz = BuildPacket(MSGID_USERAUTH_FAILURE, buf, sizeof(buf)); ++ allowed = wolfSSH_TestIsMessageAllowed(ssh, ParseMsgId(buf, sz), ++ WS_MSG_RECV); ++ AssertFalse(allowed); ++ AssertTrue(ssh->error != 0); /* should set an error / abort path */ ++} ++ ++ ++/* Reject USERAUTH_SUCCESS before the client has even sent a userauth request. */ ++static void TestPrematureUserauthSuccess(WOLFSSH* ssh) ++{ ++ int allowed; ++ ++ ResetSession(ssh); ++ ssh->connectState = CONNECT_KEYED; ++ ++ allowed = wolfSSH_TestIsMessageAllowed(ssh, MSGID_USERAUTH_SUCCESS, ++ WS_MSG_RECV); ++ AssertFalse(allowed); ++} ++ ++ ++/* Reject a spoofed sequence: bogus USERAUTH_SUCCESS followed by channel msgs. */ ++static void TestChannelSpoofSequence(WOLFSSH* ssh) ++{ ++ byte buf[32]; ++ word32 sz; ++ int allowed; ++ ++ ResetSession(ssh); ++ ssh->connectState = CONNECT_KEYED; ++ ++ sz = BuildPacket(MSGID_USERAUTH_SUCCESS, buf, sizeof(buf)); ++ allowed = wolfSSH_TestIsMessageAllowed(ssh, ParseMsgId(buf, sz), ++ WS_MSG_RECV); ++ AssertFalse(allowed); ++ ++ sz = BuildPacket(MSGID_CHANNEL_OPEN_CONF, buf, sizeof(buf)); ++ allowed = wolfSSH_TestIsMessageAllowed(ssh, ParseMsgId(buf, sz), ++ WS_MSG_RECV); ++ AssertFalse(allowed); ++ ++ sz = BuildPacket(MSGID_CHANNEL_SUCCESS, buf, sizeof(buf)); ++ allowed = wolfSSH_TestIsMessageAllowed(ssh, ParseMsgId(buf, sz), ++ WS_MSG_RECV); ++ AssertFalse(allowed); ++ ++ sz = BuildPacket(MSGID_CHANNEL_DATA, buf, sizeof(buf)); ++ allowed = wolfSSH_TestIsMessageAllowed(ssh, ParseMsgId(buf, sz), ++ WS_MSG_RECV); ++ AssertFalse(allowed); ++} ++ ++/* Expect abort/error on spoofed auth+channel sequence. */ ++static void TestChannelSpoofAborts(WOLFSSH* ssh) ++{ ++ byte buf[32]; ++ word32 sz; ++ int allowed; ++ ++ ResetSession(ssh); ++ ssh->connectState = CONNECT_KEYED; ++ ++ sz = BuildPacket(MSGID_USERAUTH_SUCCESS, buf, sizeof(buf)); ++ allowed = wolfSSH_TestIsMessageAllowed(ssh, ParseMsgId(buf, sz), ++ WS_MSG_RECV); ++ AssertFalse(allowed); ++ ++ sz = BuildPacket(MSGID_CHANNEL_OPEN_CONF, buf, sizeof(buf)); ++ allowed = wolfSSH_TestIsMessageAllowed(ssh, ParseMsgId(buf, sz), ++ WS_MSG_RECV); ++ AssertFalse(allowed); ++ ++ AssertTrue(ssh->error != 0); ++} ++ ++ ++/* Reject USERAUTH_FAILURE(publickey) before any auth request (static-signature PoC). */ ++static void TestPublicKeyFailureBeforeRequest(WOLFSSH* ssh) ++{ ++ byte buf[32]; ++ word32 sz; ++ int allowed; ++ ++ ResetSession(ssh); ++ ssh->connectState = CONNECT_KEYED; ++ ++ sz = BuildPacket(MSGID_USERAUTH_FAILURE, buf, sizeof(buf)); ++ allowed = wolfSSH_TestIsMessageAllowed(ssh, ParseMsgId(buf, sz), ++ WS_MSG_RECV); ++ AssertFalse(allowed); ++} ++ ++/* Expect abort/error when publickey failure arrives before any request. */ ++static void TestPublicKeyFailureAborts(WOLFSSH* ssh) ++{ ++ byte buf[32]; ++ word32 sz; ++ int allowed; ++ ++ ResetSession(ssh); ++ ssh->connectState = CONNECT_KEYED; ++ ++ sz = BuildPacket(MSGID_USERAUTH_FAILURE, buf, sizeof(buf)); ++ allowed = wolfSSH_TestIsMessageAllowed(ssh, ParseMsgId(buf, sz), ++ WS_MSG_RECV); ++ AssertFalse(allowed); ++ AssertTrue(ssh->error != 0); ++} ++ ++ ++/* Reject channel messages before user authentication completes. */ ++static void TestChannelBlockedBeforeAuth(WOLFSSH* ssh) ++{ ++ int allowed; ++ ++ ResetSession(ssh); ++ ssh->connectState = CONNECT_KEYED; ++ ++ allowed = wolfSSH_TestIsMessageAllowed(ssh, MSGID_CHANNEL_OPEN, ++ WS_MSG_RECV); ++ AssertFalse(allowed); ++} ++ ++ ++/* Allow channel messages after user authentication completes. */ ++static void TestChannelAllowedAfterAuth(WOLFSSH* ssh) ++{ ++ int allowed; ++ ++ ResetSession(ssh); ++ ssh->connectState = CONNECT_SERVER_USERAUTH_ACCEPT_DONE; ++ ++ allowed = wolfSSH_TestIsMessageAllowed(ssh, MSGID_CHANNEL_OPEN, ++ WS_MSG_RECV); ++ AssertTrue(allowed); ++} ++ ++ ++/* Reject a peer KEXINIT once keying is in progress. */ ++static void TestKexInitRejectedWhenKeying(WOLFSSH* ssh) ++{ ++ int allowed; ++ ++ ResetSession(ssh); ++ ssh->isKeying = WOLFSSH_PEER_IS_KEYING; ++ ssh->connectState = CONNECT_SERVER_KEXINIT_DONE; ++ ++ allowed = wolfSSH_TestIsMessageAllowed(ssh, MSGID_KEXINIT, WS_MSG_RECV); ++ AssertFalse(allowed); ++} ++ ++ ++int main(int argc, char** argv) ++{ ++ WOLFSSH_CTX* ctx; ++ WOLFSSH* ssh; ++ ++ (void)argc; ++ (void)argv; ++ ++ wolfSSH_Init(); ++ ++ ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_CLIENT, NULL); ++ AssertNotNull(ctx); ++ ++ ssh = wolfSSH_new(ctx); ++ AssertNotNull(ssh); ++ ++ TestAuthMessageBlockedDuringKeying(ssh); ++ TestUserauthFailureDuringKeying(ssh); ++ TestPasswordLeakAborts(ssh); ++ TestPrematureUserauthSuccess(ssh); ++ TestChannelSpoofSequence(ssh); ++ TestChannelSpoofAborts(ssh); ++ TestPublicKeyFailureBeforeRequest(ssh); ++ TestPublicKeyFailureAborts(ssh); ++ TestChannelBlockedBeforeAuth(ssh); ++ TestChannelAllowedAfterAuth(ssh); ++ TestKexInitRejectedWhenKeying(ssh); ++ ++ ResetSession(ssh); ++ wolfSSH_free(ssh); ++ wolfSSH_CTX_free(ctx); ++ wolfSSH_Cleanup(); ++ ++ printf("regress: PASS\n"); ++ return 0; ++} +diff --git a/wolfssh/internal.h b/wolfssh/internal.h +index 9bba76367..fa3a2d590 100644 +--- a/wolfssh/internal.h ++++ b/wolfssh/internal.h +@@ -1300,6 +1300,11 @@ enum WS_MessageIdLimits { + #define WS_MSG_SEND 1 + #define WS_MSG_RECV 2 + ++#ifdef WOLFSSH_TEST_INTERNAL ++ WOLFSSH_API int wolfSSH_TestIsMessageAllowed(WOLFSSH* ssh, byte msg, ++ byte state); ++#endif ++ + /* dynamic memory types */ + enum WS_DynamicTypes { + DYNTYPE_STRING = 500, + +From dee1c59f263220ecd05a88a72da104c22bef0598 Mon Sep 17 00:00:00 2001 +From: Andrew Hutchings +Date: Mon, 1 Dec 2025 15:41:13 +0000 +Subject: [PATCH 4/7] Fix double-free crash and socket-close spin + +--- + apps/wolfssh/common.c | 35 +++++++++++++++++-- + apps/wolfssh/wolfssh.c | 79 ++++++++++++++++++++++++++++-------------- + src/internal.c | 2 ++ + tests/include.am | 3 +- + tests/regress.c | 47 +++++++++++++++++++++++++ + 5 files changed, 137 insertions(+), 29 deletions(-) + +diff --git a/apps/wolfssh/common.c b/apps/wolfssh/common.c +index 283708cc1..e8e347a40 100644 +--- a/apps/wolfssh/common.c ++++ b/apps/wolfssh/common.c +@@ -45,10 +45,12 @@ static byte* userPublicKey = userPublicKeyBuf; + static const byte* userPublicKeyType = NULL; + static byte userPassword[256]; + static const byte* userPrivateKeyType = NULL; ++static byte userPublicKeyAlloc = 0; + static word32 userPublicKeySz = 0; + static byte pubKeyLoaded = 0; /* was a public key loaded */ + static byte userPrivateKeyBuf[1191]; + static byte* userPrivateKey = userPrivateKeyBuf; ++static byte userPrivateKeyAlloc = 0; + static word32 userPublicKeyTypeSz = 0; + static word32 userPrivateKeySz = sizeof(userPrivateKeyBuf); + static word32 userPrivateKeyTypeSz = 0; +@@ -670,6 +672,13 @@ int ClientUseCert(const char* certName) + userPublicKeyType = publicKeyType; + userPublicKeyTypeSz = (word32)WSTRLEN((const char*)publicKeyType); + pubKeyLoaded = 1; ++ userPublicKeyAlloc = 1; ++ } ++ else { ++ userPublicKey = userPublicKeyBuf; ++ userPublicKeySz = 0; ++ userPublicKeyType = NULL; ++ userPublicKeyAlloc = 0; + } + #else + fprintf(stderr, "Certificate support not compiled in"); +@@ -687,12 +696,22 @@ int ClientSetPrivateKey(const char* privKeyName) + { + int ret; + ++ userPrivateKeyAlloc = 0; + userPrivateKey = NULL; /* create new buffer based on parsed input */ + ret = wolfSSH_ReadKey_file(privKeyName, + (byte**)&userPrivateKey, &userPrivateKeySz, + (const byte**)&userPrivateKeyType, &userPrivateKeyTypeSz, + &isPrivate, NULL); + ++ if (ret == 0) { ++ userPrivateKeyAlloc = 1; ++ } ++ else { ++ userPrivateKey = userPrivateKeyBuf; ++ userPrivateKeySz = sizeof(userPrivateKeyBuf); ++ userPrivateKeyType = NULL; ++ } ++ + return ret; + } + +@@ -703,6 +722,7 @@ int ClientUsePubKey(const char* pubKeyName) + { + int ret; + ++ userPublicKeyAlloc = 0; + userPublicKey = NULL; /* create new buffer based on parsed input */ + ret = wolfSSH_ReadKey_file(pubKeyName, + &userPublicKey, &userPublicKeySz, +@@ -711,6 +731,11 @@ int ClientUsePubKey(const char* pubKeyName) + + if (ret == 0) { + pubKeyLoaded = 1; ++ userPublicKeyAlloc = 1; ++ } ++ else { ++ userPublicKey = userPublicKeyBuf; ++ userPublicKeySz = 0; + } + + return ret; +@@ -747,11 +772,17 @@ int ClientLoadCA(WOLFSSH_CTX* ctx, const char* caCert) + + void ClientFreeBuffers(void) + { +- if (userPublicKey != userPublicKeyBuf) { ++ if (userPublicKeyAlloc && userPublicKey != NULL) { + WFREE(userPublicKey, NULL, DYNTYPE_PRIVKEY); ++ userPublicKey = userPublicKeyBuf; ++ userPublicKeySz = 0; ++ userPublicKeyAlloc = 0; + } + +- if (userPrivateKey != userPrivateKeyBuf) { ++ if (userPrivateKeyAlloc && userPrivateKey != NULL) { + WFREE(userPrivateKey, NULL, DYNTYPE_PRIVKEY); ++ userPrivateKey = userPrivateKeyBuf; ++ userPrivateKeySz = sizeof(userPrivateKeyBuf); ++ userPrivateKeyAlloc = 0; + } + } +diff --git a/apps/wolfssh/wolfssh.c b/apps/wolfssh/wolfssh.c +index dc2d83bc9..ca405d41b 100644 +--- a/apps/wolfssh/wolfssh.c ++++ b/apps/wolfssh/wolfssh.c +@@ -220,6 +220,7 @@ typedef struct thread_args { + wolfSSL_Mutex lock; + byte rawMode; + byte quit; ++ int readError; + } thread_args; + + #ifdef _POSIX_THREADS +@@ -390,6 +391,7 @@ static THREAD_RET readPeer(void* in) + int bufSz = sizeof(buf); + thread_args* args = (thread_args*)in; + int ret = 0; ++ int stop = 0; + int fd = wolfSSH_get_fd(args->ssh); + word32 bytes; + #ifdef USE_WINDOWS_API +@@ -398,11 +400,6 @@ static THREAD_RET readPeer(void* in) + fd_set readSet; + fd_set errSet; + +- FD_ZERO(&readSet); +- FD_ZERO(&errSet); +- FD_SET(fd, &readSet); +- FD_SET(fd, &errSet); +- + #ifdef USE_WINDOWS_API + if (args->rawMode == 0) { + DWORD wrd; +@@ -431,9 +428,13 @@ static THREAD_RET readPeer(void* in) + #endif + + while (ret >= 0) { +- #if defined(WOLFSSH_TERM) && defined(USE_WINDOWS_API) ++#if defined(WOLFSSH_TERM) && defined(USE_WINDOWS_API) + (void)windowMonitor(args); +- #endif ++#endif ++ FD_ZERO(&readSet); ++ FD_ZERO(&errSet); ++ FD_SET(fd, &readSet); ++ FD_SET(fd, &errSet); + + bytes = select(fd + 1, &readSet, NULL, &errSet, NULL); + wc_LockMutex(&args->lock); +@@ -458,18 +459,18 @@ static THREAD_RET readPeer(void* in) + } while (ret > 0); + } + else if (ret <= 0) { +- if (ret == WS_FATAL_ERROR) { +- ret = wolfSSH_get_error(args->ssh); +- if (ret == WS_WANT_READ) { +- continue; +- } +- #ifdef WOLFSSH_AGENT +- else if (ret == WS_CHAN_RXD) { +- byte agentBuf[512]; +- int rxd, txd; +- word32 channel = 0; ++ int err = (ret == WS_FATAL_ERROR) ? ++ wolfSSH_get_error(args->ssh) : ret; ++ if (err == WS_WANT_READ) { ++ bytes = 0; ++ } ++#ifdef WOLFSSH_AGENT ++ else if (err == WS_CHAN_RXD) { ++ byte agentBuf[512]; ++ int rxd, txd; ++ word32 channel = 0; + +- wolfSSH_GetLastRxId(args->ssh, &channel); ++ wolfSSH_GetLastRxId(args->ssh, &channel); + rxd = wolfSSH_ChannelIdRead(args->ssh, channel, + agentBuf, sizeof(agentBuf)); + if (rxd > 4) { +@@ -495,9 +496,17 @@ static THREAD_RET readPeer(void* in) + WMEMSET(agentBuf, 0, sizeof(agentBuf)); + continue; + } +- #endif /* WOLFSSH_AGENT */ ++#endif /* WOLFSSH_AGENT */ ++ else if (err == WS_CBIO_ERR_CONN_CLOSE || ++ err == WS_SOCKET_ERROR_E || ++ err == WS_MSGID_NOT_ALLOWED_E) { ++ args->readError = err; ++ ret = err; ++ stop = 1; ++ bytes = 0; + } +- else if (ret != WS_EOF) { ++ else if (err != WS_EOF) { ++ wc_UnLockMutex(&args->lock); + err_sys("Stream read failed."); + } + } +@@ -517,12 +526,16 @@ static THREAD_RET readPeer(void* in) + } + #endif + } +- ret = wolfSSH_stream_peek(args->ssh, buf, bufSz); +- if (ret <= 0) { +- bytes = 0; /* read it all */ ++ if (!stop) { ++ ret = wolfSSH_stream_peek(args->ssh, buf, bufSz); ++ if (ret <= 0) { ++ bytes = 0; /* read it all */ ++ } + } + } + wc_UnLockMutex(&args->lock); ++ if (stop) ++ break; + } + #if !defined(WOLFSSH_NO_ECC) && defined(FP_ECC) && defined(HAVE_THREAD_LS) + wc_ecc_fp_free(); /* free per thread cache */ +@@ -791,7 +804,8 @@ static int config_parse_command_line(struct config* config, + if (found != NULL) { + *found = '\0'; + if (config->user) { +- free(config->user); ++ WFREE(config->user, NULL, 0); ++ config->user = NULL; + } + sz = WSTRLEN(cursor); + config->user = (char*)WMALLOC(sz + 1, NULL, 0); +@@ -818,7 +832,7 @@ static int config_parse_command_line(struct config* config, + strcpy(config->hostname, cursor); + } + +- free(dest); ++ WFREE(dest, NULL, 0); + myoptind++; + } + +@@ -874,18 +888,23 @@ static int config_cleanup(struct config* config) + { + if (config->user) { + WFREE(config->user, NULL, 0); ++ config->user = NULL; + } + if (config->hostname) { + WFREE(config->hostname, NULL, 0); ++ config->hostname = NULL; + } + if (config->keyFile) { + WFREE(config->keyFile, NULL, 0); ++ config->keyFile = NULL; + } + if (config->pubKeyFile) { + WFREE(config->pubKeyFile, NULL, 0); ++ config->pubKeyFile = NULL; + } + if (config->command) { + WFREE(config->command, NULL, 0); ++ config->command = NULL; + } + + return 0; +@@ -900,6 +919,7 @@ static THREAD_RETURN WOLFSSH_THREAD wolfSSH_Client(void* args) + SOCKADDR_IN_T clientAddr; + socklen_t clientAddrSz = sizeof(clientAddr); + int ret = 0; ++ int ioErr = 0; + byte keepOpen = 1; + #ifdef USE_WINDOWS_API + byte rawMode = 0; +@@ -1037,6 +1057,7 @@ static THREAD_RETURN WOLFSSH_THREAD wolfSSH_Client(void* args) + + wc_InitMutex(&arg.lock); + arg.ssh = ssh; ++ arg.readError = 0; + #ifdef WOLFSSH_TERM + arg.quit = 0; + #if (defined(__OSX__) || defined(__APPLE__)) +@@ -1082,12 +1103,14 @@ static THREAD_RETURN WOLFSSH_THREAD wolfSSH_Client(void* args) + sem_destroy(&windowSem); + #endif + #endif /* WOLFSSH_TERM */ ++ ioErr = arg.readError; + #elif defined(_MSC_VER) + thread_args arg; + HANDLE thread[2]; + + arg.ssh = ssh; + arg.rawMode = rawMode; ++ arg.readError = 0; + wc_InitMutex(&arg.lock); + + if (config.command) { +@@ -1107,6 +1130,7 @@ static THREAD_RETURN WOLFSSH_THREAD wolfSSH_Client(void* args) + WaitForSingleObject(thread[1], INFINITE); + CloseHandle(thread[0]); + CloseHandle(thread[1]); ++ ioErr = arg.readError; + #else + err_sys("No threading to use"); + #endif +@@ -1139,10 +1163,13 @@ static THREAD_RETURN WOLFSSH_THREAD wolfSSH_Client(void* args) + #if defined(WOLFSSH_TERM) || defined(WOLFSSH_SHELL) + ((func_args*)args)->return_code = wolfSSH_GetExitStatus(ssh); + #endif ++ if (ioErr != 0 && ((func_args*)args)->return_code == 0) { ++ ((func_args*)args)->return_code = 1; ++ } + + wolfSSH_free(ssh); + wolfSSH_CTX_free(ctx); +- if (ret != WS_SUCCESS && ret != WS_SOCKET_ERROR_E) { ++ if ((ret != WS_SUCCESS && ret != WS_SOCKET_ERROR_E) || ioErr != 0) { + WLOG(WS_LOG_DEBUG, "Closing client stream failed"); + #if defined(WOLFSSH_TERM) || defined(WOLFSSH_SHELL) + /* override return value, do not want to return success if connection +diff --git a/src/internal.c b/src/internal.c +index dfadcedea..5a162f3ce 100644 +--- a/src/internal.c ++++ b/src/internal.c +@@ -691,6 +691,8 @@ INLINE static int IsMessageAllowedClient(WOLFSSH *ssh, byte msg) + + /* Error if expecting a specific message and didn't receive. */ + if (ssh->handshake && ssh->handshake->expectMsgId != MSGID_NONE) { ++ /* The explicit expectMsgId check supersedes the old ++ * IsMessageAllowedKeying() stub for rekey filtering. */ + if (msg != ssh->handshake->expectMsgId) { + WLOG(WS_LOG_DEBUG, + "Message ID %u not the expected message %u", +diff --git a/tests/include.am b/tests/include.am +index a9aed41c0..644b4af59 100644 +--- a/tests/include.am ++++ b/tests/include.am +@@ -45,7 +45,8 @@ tests_kex_test_CPPFLAGS = -DNO_MAIN_DRIVER $(AM_CPPFLAGS) + tests_kex_test_LDADD = src/libwolfssh.la + tests_kex_test_DEPENDENCIES = src/libwolfssh.la + +-tests_regress_test_SOURCES = tests/regress.c ++tests_regress_test_SOURCES = tests/regress.c apps/wolfssh/common.c \ ++ apps/wolfssh/common.h + tests_regress_test_CPPFLAGS = -DNO_MAIN_DRIVER -DWOLFSSH_TEST_INTERNAL $(AM_CPPFLAGS) + tests_regress_test_LDADD = src/libwolfssh_test.la + tests_regress_test_DEPENDENCIES = src/libwolfssh_test.la +diff --git a/tests/regress.c b/tests/regress.c +index fbd6032e5..0fe4218f2 100644 +--- a/tests/regress.c ++++ b/tests/regress.c +@@ -19,10 +19,13 @@ + #include + #include + #include ++#include ++#include + + #include + #include + #include ++#include "apps/wolfssh/common.h" + + #ifndef WOLFSSH_NO_ABORT + #define WABORT() abort() +@@ -362,6 +365,44 @@ static void TestKexInitRejectedWhenKeying(WOLFSSH* ssh) + AssertFalse(allowed); + } + ++/* Ensure client buffer cleanup tolerates multiple invocations after allocs. */ ++static void TestClientBuffersIdempotent(void) ++{ ++ int ret; ++ ++ ret = ClientUsePubKey("keys/gretel-key-rsa.pub"); ++ AssertIntEQ(ret, 0); ++ ret = ClientSetPrivateKey("keys/gretel-key-rsa.pem"); ++ AssertIntEQ(ret, 0); ++ ++ ClientFreeBuffers(); ++ /* Should be safe to call again without double free. */ ++ ClientFreeBuffers(); ++} ++ ++/* Simulate Ctrl+D (stdin EOF) during password prompt; expect failure but no crash. */ ++static void TestPasswordEofNoCrash(void) ++{ ++ WS_UserAuthData auth; ++ int savedStdin, devNull, ret; ++ ++ WMEMSET(&auth, 0, sizeof(auth)); ++ ++ savedStdin = dup(STDIN_FILENO); ++ devNull = open("/dev/null", O_RDONLY); ++ AssertTrue(devNull >= 0); ++ AssertTrue(dup2(devNull, STDIN_FILENO) >= 0); ++ ++ ret = ClientUserAuth(WOLFSSH_USERAUTH_PASSWORD, &auth, NULL); ++ AssertIntEQ(ret, WOLFSSH_USERAUTH_FAILURE); ++ ++ close(devNull); ++ dup2(savedStdin, STDIN_FILENO); ++ close(savedStdin); ++ ++ ClientFreeBuffers(); ++} ++ + + int main(int argc, char** argv) + { +@@ -390,6 +431,12 @@ int main(int argc, char** argv) + TestChannelBlockedBeforeAuth(ssh); + TestChannelAllowedAfterAuth(ssh); + TestKexInitRejectedWhenKeying(ssh); ++ TestClientBuffersIdempotent(); ++ TestPasswordEofNoCrash(); ++ ++ /* TODO: add app-level regressions that simulate stdin EOF/password ++ * prompts and mid-session socket closes once the test harness can ++ * drive the wolfssh client without real sockets/tty. */ + + ResetSession(ssh); + wolfSSH_free(ssh); + +From 2e5484f36f279484ca06e02ef34ddd420fc0cf87 Mon Sep 17 00:00:00 2001 +From: John Safranek +Date: Thu, 4 Dec 2025 15:52:39 -0800 +Subject: [PATCH 5/7] Server Out of Order Message Handling + +1. Updated the checking for the server to be more like the client's + checking. +--- + src/internal.c | 59 +++++++++++++++++++++++++++++++++++++++++++++++++- + 1 file changed, 58 insertions(+), 1 deletion(-) + +diff --git a/src/internal.c b/src/internal.c +index 5a162f3ce..c3e6c41e3 100644 +--- a/src/internal.c ++++ b/src/internal.c +@@ -601,15 +601,71 @@ INLINE static int IsMessageAllowedServer(WOLFSSH *ssh, byte msg) + /* Only the server should send these messages, never receive. */ + if (msg == MSGID_SERVICE_ACCEPT) { + WLOG(WS_LOG_DEBUG, "Message ID %u not allowed by %s %s", +- msg, "client", "ever"); ++ msg, "server", "ever"); ++ ssh->error = WS_MSGID_NOT_ALLOWED_E; + return 0; + } + ++ if (msg == MSGID_SERVICE_REQUEST) { ++ if (ssh->acceptState == ACCEPT_KEYED) { ++ return 1; ++ } ++ else { ++ WLOG(WS_LOG_DEBUG, "Message ID %u not allowed by %s %s", ++ msg, "server", "after starting user auth"); ++ return 0; ++ } ++ } ++ + /* Transport Layer Generic messages are always allowed. */ + if (MSGIDLIMIT_TRANS_GEN(msg)) { + return 1; + } + ++ /* Is KEX complete? */ ++ if (MSGIDLIMIT_TRANS(msg)) { ++ if (ssh->isKeying & WOLFSSH_PEER_IS_KEYING) { ++ /* MSGID_KEXINIT not allowed when keying. */ ++ if (msg == MSGID_KEXINIT) { ++ WLOG(WS_LOG_DEBUG, "Message ID %u not allowed by %s %s", ++ msg, "server", "when keying"); ++ ssh->error = WS_REKEYING; ++ return 0; ++ } ++ ++ /* Error if expecting a specific message and didn't receive. */ ++ if (ssh->handshake && ssh->handshake->expectMsgId != MSGID_NONE) { ++ /* The explicit expectMsgId check supersedes the old ++ * IsMessageAllowedKeying() stub for rekey filtering. */ ++ if (msg != ssh->handshake->expectMsgId) { ++ WLOG(WS_LOG_DEBUG, ++ "Message ID %u not the expected message %u", ++ msg, ssh->handshake->expectMsgId); ++ ssh->error = WS_REKEYING; ++ return 0; ++ } ++ else { ++ /* Got the expected message, clear expectation. */ ++ ssh->handshake->expectMsgId = MSGID_NONE; ++ return 1; ++ } ++ } ++ } ++ else { ++ /* MSGID_KEXINIT only allowed when not keying. */ ++ if (msg == MSGID_KEXINIT) { ++ return 1; ++ } ++ ++ /* All other transport KEX and ALGO messages are not allowed ++ * when not keying. */ ++ WLOG(WS_LOG_DEBUG, "Message ID %u not allowed by %s %s", ++ msg, "server", "when not keying"); ++ ssh->error = WS_MSGID_NOT_ALLOWED_E; ++ return 0; ++ } ++ } ++ + /* Has client userauth started? */ + /* Allows the server to receive up to KEXDH GEX Request during KEX. */ + if (ssh->acceptState < ACCEPT_KEYED) { +@@ -617,6 +673,7 @@ INLINE static int IsMessageAllowedServer(WOLFSSH *ssh, byte msg) + return 0; + } + } ++ + /* Is server userauth complete? */ + if (ssh->acceptState < ACCEPT_SERVER_USERAUTH_SENT) { + /* The server should only receive the user auth request message, + +From 03ca9221a36a2a7160a99aeee7bc7ea8368013b5 Mon Sep 17 00:00:00 2001 +From: John Safranek +Date: Tue, 9 Dec 2025 09:17:47 -0800 +Subject: [PATCH 6/7] Zephyr + +1. Exclude the file regress.c from the Zephyr testing sample. The test + is covered in many other environments already. The test needs some + retooling to fit in with the Zephyr build, as it is a standalone + application with a main() function and it depends on a testing build + of libwolfssh. +2. Whitespace. +--- + zephyr/samples/tests/CMakeLists.txt | 8 ++++++-- + 1 file changed, 6 insertions(+), 2 deletions(-) + +diff --git a/zephyr/samples/tests/CMakeLists.txt b/zephyr/samples/tests/CMakeLists.txt +index 4f563b495..3de00b745 100644 +--- a/zephyr/samples/tests/CMakeLists.txt ++++ b/zephyr/samples/tests/CMakeLists.txt +@@ -4,8 +4,12 @@ find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE}) + project(wolfssl_tests) + + FILE(GLOB app_sources ../../../tests/*.c ../../../examples/client/client.c +- ../../../examples/client/common.c ../../../examples/echoserver/echoserver.c +- ../../../examples/sftpclient/sftpclient.c tests.c) ++ ../../../examples/client/common.c ../../../examples/echoserver/echoserver.c ++ ../../../examples/sftpclient/sftpclient.c tests.c) ++# Remove the file regress.c from the list of test app sources. The regression ++# test is covered in many other environments. ++list(REMOVE_ITEM app_sources ++ "${CMAKE_CURRENT_SOURCE_DIR}/../../../tests/regress.c") + target_sources(app PRIVATE ${app_sources}) + add_definitions(-DWOLFSSL_ZEPHYR) + add_definitions(-DWOLFSSL_USER_SETTINGS) + +From 2086f34ff37911a1fa388f843ee9d336e740bf44 Mon Sep 17 00:00:00 2001 +From: John Safranek +Date: Thu, 11 Dec 2025 10:43:35 -0800 +Subject: [PATCH 7/7] Out of Order Message Handling + +1. Always set the expected message right before sending. If the send + fails, it is either because the socket is closing, or it is wanting + to block. If it is wanting to block, we still want to check the next + message as expected. +--- + src/internal.c | 11 ++++------- + 1 file changed, 4 insertions(+), 7 deletions(-) + +diff --git a/src/internal.c b/src/internal.c +index c3e6c41e3..fb67a53c8 100644 +--- a/src/internal.c ++++ b/src/internal.c +@@ -5989,8 +5989,9 @@ static int DoKexDhReply(WOLFSSH* ssh, byte* buf, word32 len, word32* idx) + } + + if (ret == WS_SUCCESS) { +- ret = SendNewKeys(ssh); + ssh->handshake->expectMsgId = MSGID_NEWKEYS; ++ WLOG_EXPECT_MSGID(ssh->handshake->expectMsgId); ++ ret = SendNewKeys(ssh); + } + + if (sigKeyBlock_ptr) +@@ -12729,12 +12730,10 @@ int SendKexDhGexRequest(WOLFSSH* ssh) + ret = BundlePacket(ssh); + } + +- if (ret == WS_SUCCESS) +- ret = wolfSSH_SendPacket(ssh); +- + if (ret == WS_SUCCESS) { + WLOG_EXPECT_MSGID(MSGID_KEXDH_GEX_GROUP); + ssh->handshake->expectMsgId = MSGID_KEXDH_GEX_GROUP; ++ ret = wolfSSH_SendPacket(ssh); + } + + WLOG(WS_LOG_DEBUG, "Leaving SendKexDhGexRequest(), ret = %d", ret); +@@ -13086,12 +13085,10 @@ int SendKexDhInit(WOLFSSH* ssh) + ret = BundlePacket(ssh); + } + +- if (ret == WS_SUCCESS) +- ret = wolfSSH_SendPacket(ssh); +- + if (ret == WS_SUCCESS) { + WLOG_EXPECT_MSGID(expectMsgId); + ssh->handshake->expectMsgId = expectMsgId; ++ ret = wolfSSH_SendPacket(ssh); + } + + WLOG(WS_LOG_DEBUG, "Leaving SendKexDhInit(), ret = %d", ret); diff --git a/tools/wolfssh_order/921.patch b/tools/wolfssh_order/921.patch new file mode 100644 index 0000000..a69cc45 --- /dev/null +++ b/tools/wolfssh_order/921.patch @@ -0,0 +1,32 @@ +From e9d288ec603531a1d544e77fb1bbdf634cb2a57f Mon Sep 17 00:00:00 2001 +From: John Safranek +Date: Mon, 13 Apr 2026 15:05:48 -0700 +Subject: [PATCH] Server Does Not Set expectMsgId to MSGID_NEWKEYS + +In the server code, the server is not setting the expectedMsgId to +MSGID_NEWKEYS before sending its new keys message. Update DoKexDhReply() +to set expectMsgId to MSGID_NEWKEYS. + +Affected function: DoKeyDhReply. +Issue: F-1275 +--- + src/internal.c | 5 ++++- + 1 file changed, 4 insertions(+), 1 deletion(-) + +diff --git a/src/internal.c b/src/internal.c +index 970bece9b..511d77ef2 100644 +--- a/src/internal.c ++++ b/src/internal.c +@@ -12914,8 +12914,11 @@ int SendKexDhReply(WOLFSSH* ssh) + ret = BundlePacket(ssh); + } + +- if (ret == WS_SUCCESS) ++ if (ret == WS_SUCCESS) { ++ ssh->handshake->expectMsgId = MSGID_NEWKEYS; ++ WLOG_EXPECT_MSGID(ssh->handshake->expectMsgId); + ret = SendNewKeys(ssh); ++ } + + if (ret == WS_SUCCESS && ssh->sendExtInfo) { + ret = SendExtInfo(ssh); diff --git a/tools/wolfssh_order/README.md b/tools/wolfssh_order/README.md new file mode 100644 index 0000000..6d8b643 --- /dev/null +++ b/tools/wolfssh_order/README.md @@ -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/`. + +`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 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: + +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. diff --git a/tools/wolfssh_order/delta.json b/tools/wolfssh_order/delta.json new file mode 100644 index 0000000..205a9a3 --- /dev/null +++ b/tools/wolfssh_order/delta.json @@ -0,0 +1,161 @@ +{ + "src/internal.c": { + "sha256": "81ff1f9166708abd5c2911e9fe57c0aee01c88b5d3f68c909ee8a856d37f36a9", + "edits": [ + { + "old": " * API calls into this module to do the work of processing the connections.\n */\n\n\n#ifdef HAVE_CONFIG_H\n #include \n#endif\n\n#include \n#include \n#include \n#include \n#include \n#include \n#ifndef WOLFSSH_NO_DH\n #include \n#endif\n#include \n#include \n#ifdef WOLFSSH_CERTS\n #include \n#endif\n#include \n#include \n", + "new": " * API calls into this module to do the work of processing the connections.\n */\n\n\n#ifdef HAVE_CONFIG_H\n #include \n#endif\n\n#include \n#include \n#include \n#include \n#define WLOG_EXPECT_MSGID(x) WLOG(WS_LOG_DEBUG, \"Expecting message %d\", (x))\n#include \n#include \n#ifndef WOLFSSH_NO_DH\n #include \n#endif\n#include \n#include \n#ifdef WOLFSSH_CERTS\n #include \n#endif\n#include \n#include \n" + }, + { + "old": " WFREE(hs->primeGroup, heap, DYNTYPE_MPINT);\n WFREE(hs->generator, heap, DYNTYPE_MPINT);\n#endif\n if (hs->kexHashId != WC_HASH_TYPE_NONE) {\n wc_HashFree(&hs->kexHash, (enum wc_HashType)hs->kexHashId);\n }\n ForceZero(hs, sizeof(HandshakeInfo));\n WFREE(hs, heap, DYNTYPE_HS);\n }\n}\n\n\n#ifndef NO_WOLFSSH_SERVER\nINLINE static int IsMessageAllowedServer(WOLFSSH *ssh, byte msg)\n{\n /* Has client userauth started? */\n if (ssh->acceptState < ACCEPT_KEYED) {\n if (msg > MSGID_KEXDH_LIMIT) {\n return 0;\n }\n }\n /* Is server userauth complete? */\n if (ssh->acceptState < ACCEPT_SERVER_USERAUTH_SENT) {\n /* Explicitly check for messages not allowed before user\n * authentication has comleted. */\n if (msg >= MSGID_USERAUTH_LIMIT) {\n WLOG(WS_LOG_DEBUG, \"Message ID %u not allowed by server \"\n \"before user authentication is complete\", msg);\n return 0;\n }\n /* Explicitly check for the user authentication messages that\n * only the server sends, it shouldn't receive them. */\n if ((msg > MSGID_USERAUTH_RESTRICT) &&\n (msg != MSGID_USERAUTH_INFO_RESPONSE)) {\n WLOG(WS_LOG_DEBUG, \"Message ID %u not allowed by server \"\n \"during user authentication\", msg);\n return 0;\n }\n }\n else {\n if (msg >= MSGID_USERAUTH_RESTRICT && msg < MSGID_USERAUTH_LIMIT) {\n WLOG(WS_LOG_DEBUG, \"Message ID %u not allowed by server \"\n \"after user authentication\", msg);\n return 0;\n }\n }\n\n return 1;\n}\n#endif /* NO_WOLFSSH_SERVER */\n\n\n#ifndef NO_WOLFSSH_CLIENT\nINLINE static int IsMessageAllowedClient(WOLFSSH *ssh, byte msg)\n{\n /* Has client userauth started? */\n if (ssh->connectState < CONNECT_CLIENT_KEXDH_INIT_SENT) {\n if (msg >= MSGID_KEXDH_LIMIT) {\n return 0;\n }\n }\n /* Is client userauth complete? */\n if (ssh->connectState < CONNECT_SERVER_USERAUTH_ACCEPT_DONE) {\n /* Explicitly check for messages not allowed before user\n * authentication has comleted. */\n if (msg >= MSGID_USERAUTH_LIMIT) {\n WLOG(WS_LOG_DEBUG, \"Message ID %u not allowed by client \"\n \"before user authentication is complete\", msg);\n return 0;\n }\n /* Explicitly check for the user authentication message that\n * only the client sends, it shouldn't receive it. */\n if (msg == MSGID_USERAUTH_RESTRICT) {\n WLOG(WS_LOG_DEBUG, \"Message ID %u not allowed by client \"\n \"during user authentication\", msg);\n return 0;\n }\n }\n else {\n if (msg >= MSGID_USERAUTH_RESTRICT && msg < MSGID_USERAUTH_LIMIT) {\n WLOG(WS_LOG_DEBUG, \"Message ID %u not allowed by client \"\n \"after user authentication\", msg);\n return 0;\n }\n }\n return 1;\n}\n#endif /* NO_WOLFSSH_CLIENT */\n\n\nINLINE static int IsMessageAllowed(WOLFSSH *ssh, byte msg)\n{\n#ifndef NO_WOLFSSH_SERVER\n if (ssh->ctx->side == WOLFSSH_ENDPOINT_SERVER) {\n return IsMessageAllowedServer(ssh, msg);\n }\n#endif /* NO_WOLFSSH_SERVER */\n#ifndef NO_WOLFSSH_CLIENT\n if (ssh->ctx->side == WOLFSSH_ENDPOINT_CLIENT) {\n return IsMessageAllowedClient(ssh, msg);\n }\n#endif /* NO_WOLFSSH_CLIENT */\n return 0;\n}\n\n\nstatic const char cannedKexAlgoNames[] =\n#if !defined(WOLFSSH_NO_ECDH_NISTP256_KYBER_LEVEL1_SHA256)\n \"ecdh-nistp256-kyber-512r3-sha256-d00@openquantumsafe.org,\"\n#endif\n#ifndef WOLFSSH_NO_CURVE25519_SHA256\n \"curve25519-sha256,\"\n#endif\n#if !defined(WOLFSSH_NO_ECDH_SHA2_NISTP521)\n", + "new": " WFREE(hs->primeGroup, heap, DYNTYPE_MPINT);\n WFREE(hs->generator, heap, DYNTYPE_MPINT);\n#endif\n if (hs->kexHashId != WC_HASH_TYPE_NONE) {\n wc_HashFree(&hs->kexHash, (enum wc_HashType)hs->kexHashId);\n }\n ForceZero(hs, sizeof(HandshakeInfo));\n WFREE(hs, heap, DYNTYPE_HS);\n }\n}\n\n\n/* Project restricted ordering profile, derived from wolfSSH PR855.\n * EXT_INFO is deliberately not negotiated. See README.md for scope.\n * This fragment is installed into the pinned internal.c by the exact delta.\n */\n#ifndef NO_WOLFSSH_SERVER\nINLINE static int IsMessageAllowedServer(WOLFSSH* ssh, byte msg)\n{\n if (msg == MSGID_SERVICE_REQUEST)\n return ssh->acceptState == ACCEPT_KEYED;\n if (msg == MSGID_USERAUTH_REQUEST)\n return ssh->acceptState >= ACCEPT_SERVER_USERAUTH_ACCEPT_SENT &&\n ssh->acceptState < ACCEPT_SERVER_USERAUTH_SENT;\n /* Keyboard-interactive is not part of the project profile. */\n return MSGIDLIMIT_POST_USERAUTH(msg) &&\n ssh->acceptState >= ACCEPT_SERVER_USERAUTH_SENT;\n}\n#endif\n\n#ifndef NO_WOLFSSH_CLIENT\nINLINE static int IsMessageAllowedClient(WOLFSSH* ssh, byte msg)\n{\n if (msg == MSGID_SERVICE_ACCEPT)\n return ssh->connectState == CONNECT_CLIENT_USERAUTH_REQUEST_SENT;\n if (msg == MSGID_USERAUTH_FAILURE || msg == MSGID_USERAUTH_SUCCESS ||\n msg == MSGID_USERAUTH_BANNER || msg == MSGID_USERAUTH_PK_OK)\n return ssh->connectState >= CONNECT_CLIENT_USERAUTH_SENT &&\n ssh->connectState < CONNECT_SERVER_USERAUTH_ACCEPT_DONE;\n return MSGIDLIMIT_POST_USERAUTH(msg) &&\n ssh->connectState >= CONNECT_SERVER_USERAUTH_ACCEPT_DONE;\n}\n#endif\n\nINLINE static int IsMessageAllowed(WOLFSSH* ssh, byte msg, byte state)\n{\n int allowed = 0;\n if (state == WS_MSG_SEND) {\n /* EOF/window-adjust callers must not consume receive expectations. */\n allowed = !ssh->isKeying;\n if (!allowed)\n ssh->error = WS_REKEYING;\n return allowed;\n }\n if (state != WS_MSG_RECV)\n goto reject;\n\n /* RFC4253 transport notifications may interrupt an expected KEX message. */\n if (msg >= MSGID_DISCONNECT && msg <= MSGID_DEBUG)\n return 1;\n\n /* No RFC8308 negotiation, including during rekey or after authentication. */\n if (msg == MSGID_EXT_INFO)\n goto reject;\n\n if (MSGIDLIMIT_TRANS_ALGO(msg) || MSGIDLIMIT_TRANS_KEX(msg)) {\n if (msg == MSGID_KEXINIT) {\n allowed = !(ssh->isKeying & WOLFSSH_PEER_IS_KEYING);\n }\n else if ((ssh->isKeying & WOLFSSH_PEER_IS_KEYING) &&\n ssh->handshake != NULL &&\n ssh->handshake->expectMsgId != MSGID_NONE &&\n ssh->handshake->expectMsgId == msg) {\n ssh->handshake->expectMsgId = MSGID_NONE;\n return 1;\n }\n if (allowed)\n return 1;\n goto reject;\n }\n\n /* Locally initiated rekey can have old, in-flight peer traffic. Once the\n * peer KEXINIT arrives, only transport/KEX is legal until peer NEWKEYS. */\n if (ssh->isKeying & WOLFSSH_PEER_IS_KEYING)\n goto reject;\n#ifndef NO_WOLFSSH_SERVER\n if (ssh->ctx->side == WOLFSSH_ENDPOINT_SERVER)\n allowed = IsMessageAllowedServer(ssh, msg);\n#endif\n#ifndef NO_WOLFSSH_CLIENT\n if (ssh->ctx->side == WOLFSSH_ENDPOINT_CLIENT)\n allowed = IsMessageAllowedClient(ssh, msg);\n#endif\n if (allowed)\n return 1;\nreject:\n ssh->error = WS_MSGID_NOT_ALLOWED_E;\n return 0;\n}\n\n\nstatic const char cannedKexAlgoNames[] =\n#if !defined(WOLFSSH_NO_ECDH_NISTP256_KYBER_LEVEL1_SHA256)\n \"ecdh-nistp256-kyber-512r3-sha256-d00@openquantumsafe.org,\"\n#endif\n#ifndef WOLFSSH_NO_CURVE25519_SHA256\n \"curve25519-sha256,\"\n#endif\n#if !defined(WOLFSSH_NO_ECDH_SHA2_NISTP521)\n" + }, + { + "old": "#else\n ssh->rfd = -1; /* set to invalid */\n ssh->wfd = -1; /* set to invalid */\n#endif\n ssh->ioReadCtx = &ssh->rfd; /* prevent invalid access if not correctly */\n ssh->ioWriteCtx = &ssh->wfd; /* set */\n ssh->highwaterMark = ctx->highwaterMark;\n ssh->highwaterCtx = (void*)ssh;\n ssh->reqSuccessCtx = (void*)ssh;\n ssh->fs = NULL;\n ssh->acceptState = ACCEPT_BEGIN;\n ssh->clientState = CLIENT_BEGIN;\n ssh->isKeying = 1;\n ssh->authId = ID_USERAUTH_PUBLICKEY;\n ssh->supportedAuth[0] = ID_USERAUTH_PUBLICKEY;\n ssh->supportedAuth[1] = ID_USERAUTH_PASSWORD;\n ssh->supportedAuth[2] = ID_USERAUTH_KEYBOARD;\n ssh->supportedAuth[3] = ID_NONE; /* ID_NONE is treated as empty slot */\n ssh->nextChannel = DEFAULT_NEXT_CHANNEL;\n ssh->blockSz = MIN_BLOCK_SZ;\n ssh->encryptId = ID_NONE;\n ssh->macId = ID_NONE;\n ssh->peerBlockSz = MIN_BLOCK_SZ;\n ssh->rng = rng;\n ssh->kSz = (word32)sizeof(ssh->k);\n", + "new": "#else\n ssh->rfd = -1; /* set to invalid */\n ssh->wfd = -1; /* set to invalid */\n#endif\n ssh->ioReadCtx = &ssh->rfd; /* prevent invalid access if not correctly */\n ssh->ioWriteCtx = &ssh->wfd; /* set */\n ssh->highwaterMark = ctx->highwaterMark;\n ssh->highwaterCtx = (void*)ssh;\n ssh->reqSuccessCtx = (void*)ssh;\n ssh->fs = NULL;\n ssh->acceptState = ACCEPT_BEGIN;\n ssh->clientState = CLIENT_BEGIN;\n ssh->isKeying = 0; /* initial state of not keying yet */\n ssh->authId = ID_USERAUTH_PUBLICKEY;\n ssh->supportedAuth[0] = ID_USERAUTH_PUBLICKEY;\n ssh->supportedAuth[1] = ID_USERAUTH_PASSWORD;\n ssh->supportedAuth[2] = ID_USERAUTH_KEYBOARD;\n ssh->supportedAuth[3] = ID_NONE; /* ID_NONE is treated as empty slot */\n ssh->nextChannel = DEFAULT_NEXT_CHANNEL;\n ssh->blockSz = MIN_BLOCK_SZ;\n ssh->encryptId = ID_NONE;\n ssh->macId = ID_NONE;\n ssh->peerBlockSz = MIN_BLOCK_SZ;\n ssh->rng = rng;\n ssh->kSz = (word32)sizeof(ssh->k);\n" + }, + { + "old": " word32 cannedAlgoNamesSz;\n word32 skipSz = 0;\n word32 begin;\n\n WLOG(WS_LOG_DEBUG, \"Entering DoKexInit()\");\n\n if (ssh == NULL || ssh->ctx == NULL ||\n buf == NULL || len == 0 || idx == NULL) {\n\n ret = WS_BAD_ARGUMENT;\n }\n\n /*\n * I don't need to save what the client sends here. I should decode\n * each list into a local array of IDs, and pick the one the peer is\n * using that's on my known list, or verify that the one the peer can\n * support the other direction is on my known list. All I need to do\n * is save the actual values.\n */\n\n if (ret == WS_SUCCESS) {\n if (ssh->handshake == NULL) {\n ssh->handshake = HandshakeInfoNew(ssh->ctx->heap);\n if (ssh->handshake == NULL) {\n WLOG(WS_LOG_DEBUG, \"Couldn't allocate handshake info\");\n ret = WS_MEMORY_E;\n }\n }\n }\n\n if (ret == WS_SUCCESS) {\n begin = *idx;\n side = ssh->ctx->side;\n", + "new": " word32 cannedAlgoNamesSz;\n word32 skipSz = 0;\n word32 begin;\n\n WLOG(WS_LOG_DEBUG, \"Entering DoKexInit()\");\n\n if (ssh == NULL || ssh->ctx == NULL ||\n buf == NULL || len == 0 || idx == NULL) {\n\n ret = WS_BAD_ARGUMENT;\n }\n\n if (ret == WS_SUCCESS) {\n /* Check if already in process of keying and error out if so. */\n if (ssh->isKeying & WOLFSSH_PEER_IS_KEYING) {\n WLOG(WS_LOG_ERROR,\n \"Already in keying process and got KEX init\");\n ret = WS_INVALID_STATE_E;\n }\n }\n\n /*\n * I don't need to save what the client sends here. I should decode\n * each list into a local array of IDs, and pick the one the peer is\n * using that's on my known list, or verify that the one the peer can\n * support the other direction is on my known list. All I need to do\n * is save the actual values.\n */\n\n if (ret == WS_SUCCESS) {\n /* Set peer is keying flag after receiving SSH_MSG_KEX_INIT */\n ssh->isKeying |= WOLFSSH_PEER_IS_KEYING;\n if (ssh->handshake == NULL) {\n ssh->handshake = HandshakeInfoNew(ssh->ctx->heap);\n if (ssh->handshake == NULL) {\n WLOG(WS_LOG_DEBUG, \"Couldn't allocate handshake info\");\n ret = WS_MEMORY_E;\n }\n }\n }\n\n if (ret == WS_SUCCESS) {\n begin = *idx;\n side = ssh->ctx->side;\n" + }, + { + "old": " (const byte*)ssh->algoListKex, cannedAlgoNamesSz);\n }\n if (ret == WS_SUCCESS) {\n ssh->handshake->kexIdGuess = list[0];\n algoId = MatchIdLists(side, list, listSz,\n cannedList, cannedListSz);\n if (algoId == ID_UNKNOWN) {\n WLOG(WS_LOG_DEBUG, \"Unable to negotiate KEX Algo\");\n ret = WS_MATCH_KEX_ALGO_E;\n }\n }\n if (ret == WS_SUCCESS) {\n ssh->kexId = ssh->handshake->kexId = algoId;\n ssh->handshake->kexHashId = HashForId(algoId);\n }\n /* Extension Info Flag */\n if (ret == WS_SUCCESS) {\n /* Only checking for this is we are server. Our client does\n * not have anything to say to a server, yet. */\n if (side == WOLFSSH_ENDPOINT_SERVER) {\n byte extInfo;\n\n /* Match the client accepts extInfo. */\n algoId = ID_EXTINFO_C;\n extInfo = MatchIdLists(side, list, listSz, &algoId, 1);\n ssh->sendExtInfo = extInfo == algoId;\n }\n }\n\n /* Server Host Key Algorithms */\n if (ret == WS_SUCCESS) {\n WLOG(WS_LOG_DEBUG, \"DKI: Server Host Key Algorithms\");\n listSz = (word32)sizeof(list);\n ret = GetNameList(list, &listSz, buf, len, &begin);\n }\n if (ret == WS_SUCCESS) {\n if (side == WOLFSSH_ENDPOINT_SERVER && !ssh->algoListKey) {\n cannedListSz = ssh->ctx->publicKeyAlgoCount;\n WMEMCPY(cannedList, ssh->ctx->publicKeyAlgo, cannedListSz);\n }\n", + "new": " (const byte*)ssh->algoListKex, cannedAlgoNamesSz);\n }\n if (ret == WS_SUCCESS) {\n ssh->handshake->kexIdGuess = list[0];\n algoId = MatchIdLists(side, list, listSz,\n cannedList, cannedListSz);\n if (algoId == ID_UNKNOWN) {\n WLOG(WS_LOG_DEBUG, \"Unable to negotiate KEX Algo\");\n ret = WS_MATCH_KEX_ALGO_E;\n }\n }\n if (ret == WS_SUCCESS) {\n /* The reviewed project profile has only these two KEX algorithms.\n * Do not silently enable an untested GEX/PQ continuation. */\n if (algoId != ID_CURVE25519_SHA256 && algoId != ID_ECDH_SHA2_NISTP256)\n ret = WS_MATCH_KEX_ALGO_E;\n ssh->kexId = ssh->handshake->kexId = algoId;\n ssh->handshake->kexHashId = HashForId(algoId);\n }\n /* RFC8308 is optional. This profile never sends EXT_INFO, including\n * after a nonblocking NEWKEYS send or on subsequent rekeys. */\n ssh->sendExtInfo = 0;\n\n /* Server Host Key Algorithms */\n if (ret == WS_SUCCESS) {\n WLOG(WS_LOG_DEBUG, \"DKI: Server Host Key Algorithms\");\n listSz = (word32)sizeof(list);\n ret = GetNameList(list, &listSz, buf, len, &begin);\n }\n if (ret == WS_SUCCESS) {\n if (side == WOLFSSH_ENDPOINT_SERVER && !ssh->algoListKey) {\n cannedListSz = ssh->ctx->publicKeyAlgoCount;\n WMEMCPY(cannedList, ssh->ctx->publicKeyAlgo, cannedListSz);\n }\n" + }, + { + "old": " WLOG(WS_LOG_DEBUG, \"DKI: For Future Use\");\n ret = GetUint32(&skipSz, buf, len, &begin);\n if (ret == WS_SUCCESS)\n begin += skipSz;\n }\n\n if (ret == WS_SUCCESS) {\n wc_HashAlg* hash = &ssh->handshake->kexHash;\n enum wc_HashType hashId = (enum wc_HashType)ssh->handshake->kexHashId;\n byte scratchLen[LENGTH_SZ];\n word32 strSz = 0;\n\n if (!ssh->isKeying) {\n WLOG(WS_LOG_DEBUG, \"Keying initiated\");\n ret = SendKexInit(ssh);\n }\n\n /* account for possible want write case from SendKexInit */\n if (ret == WS_SUCCESS || ret == WS_WANT_WRITE)\n ret = wc_HashInit(hash, hashId);\n\n if (ret == WS_SUCCESS) {\n if (ssh->ctx->side == WOLFSSH_ENDPOINT_SERVER) {\n ret = HashUpdate(hash, hashId,\n ssh->peerProtoId, ssh->peerProtoIdSz);\n", + "new": " WLOG(WS_LOG_DEBUG, \"DKI: For Future Use\");\n ret = GetUint32(&skipSz, buf, len, &begin);\n if (ret == WS_SUCCESS)\n begin += skipSz;\n }\n\n if (ret == WS_SUCCESS) {\n wc_HashAlg* hash = &ssh->handshake->kexHash;\n enum wc_HashType hashId = (enum wc_HashType)ssh->handshake->kexHashId;\n byte scratchLen[LENGTH_SZ];\n word32 strSz = 0;\n\n /* respond with KEX Init message if not having initiated the keying */\n if ((ssh->isKeying & WOLFSSH_SELF_IS_KEYING) == 0) {\n WLOG(WS_LOG_DEBUG, \"Keying initiated\");\n ret = SendKexInit(ssh);\n }\n\n /* account for possible want write case from SendKexInit */\n if (ret == WS_SUCCESS || ret == WS_WANT_WRITE)\n ret = wc_HashInit(hash, hashId);\n\n if (ret == WS_SUCCESS) {\n if (ssh->ctx->side == WOLFSSH_ENDPOINT_SERVER) {\n ret = HashUpdate(hash, hashId,\n ssh->peerProtoId, ssh->peerProtoIdSz);\n" + }, + { + "old": "\n if (ret == WS_SUCCESS)\n ret = HashUpdate(hash, hashId, buf, len);\n\n if (ret == WS_SUCCESS) {\n if (ssh->ctx->side == WOLFSSH_ENDPOINT_SERVER)\n ret = HashUpdate(hash, hashId,\n ssh->handshake->kexInit, ssh->handshake->kexInitSz);\n }\n\n if (ret == WS_SUCCESS) {\n *idx = begin;\n if (ssh->ctx->side == WOLFSSH_ENDPOINT_SERVER)\n ssh->clientState = CLIENT_KEXINIT_DONE;\n else\n ssh->serverState = SERVER_KEXINIT_DONE;\n\n /* Propagate potential want write case from SendKexInit. */\n if (ssh->error != 0)\n ret = ssh->error;\n }\n }\n WLOG(WS_LOG_DEBUG, \"Leaving DoKexInit(), ret = %d\", ret);\n return ret;\n}\n\n", + "new": "\n if (ret == WS_SUCCESS)\n ret = HashUpdate(hash, hashId, buf, len);\n\n if (ret == WS_SUCCESS) {\n if (ssh->ctx->side == WOLFSSH_ENDPOINT_SERVER)\n ret = HashUpdate(hash, hashId,\n ssh->handshake->kexInit, ssh->handshake->kexInitSz);\n }\n\n if (ret == WS_SUCCESS) {\n *idx = begin;\n if (ssh->ctx->side == WOLFSSH_ENDPOINT_SERVER) {\n ssh->handshake->expectMsgId = MSGID_KEXDH_INIT;\n ssh->clientState = CLIENT_KEXINIT_DONE;\n }\n else\n ssh->serverState = SERVER_KEXINIT_DONE;\n\n /* Propagate potential want write case from SendKexInit. */\n if (ssh->error != 0)\n ret = ssh->error;\n }\n }\n WLOG(WS_LOG_DEBUG, \"Leaving DoKexInit(), ret = %d\", ret);\n return ret;\n}\n\n" + }, + { + "old": "\n if (ssh == NULL || ssh->handshake == NULL || buf == NULL || len == 0 ||\n idx == NULL)\n ret = WS_BAD_ARGUMENT;\n\n if (ret == WS_SUCCESS) {\n if (ssh->handshake->kexPacketFollows\n && ssh->handshake->kexIdGuess != ssh->handshake->kexId) {\n\n /* skip this message. */\n WLOG(WS_LOG_DEBUG, \"Skipping the client's KEX init function.\");\n ssh->handshake->kexPacketFollows = 0;\n *idx += len;\n return WS_SUCCESS;\n }\n }\n\n if (ret == WS_SUCCESS) {\n begin = *idx;\n ret = GetUint32(&eSz, buf, len, &begin);\n }\n\n if (ret == WS_SUCCESS) {\n /* Validate eSz */\n", + "new": "\n if (ssh == NULL || ssh->handshake == NULL || buf == NULL || len == 0 ||\n idx == NULL)\n ret = WS_BAD_ARGUMENT;\n\n if (ret == WS_SUCCESS) {\n if (ssh->handshake->kexPacketFollows\n && ssh->handshake->kexIdGuess != ssh->handshake->kexId) {\n\n /* skip this message. */\n WLOG(WS_LOG_DEBUG, \"Skipping the client's KEX init function.\");\n ssh->handshake->kexPacketFollows = 0;\n ssh->handshake->expectMsgId = MSGID_KEXDH_INIT;\n *idx += len;\n return WS_SUCCESS;\n }\n }\n\n if (ret == WS_SUCCESS) {\n begin = *idx;\n ret = GetUint32(&eSz, buf, len, &begin);\n }\n\n if (ret == WS_SUCCESS) {\n /* Validate eSz */\n" + }, + { + "old": " ret = WS_INVALID_ALGO_ID;\n }\n }\n }\n FreePubKey(sigKeyBlock_ptr);\n }\n\n if (ret == WS_SUCCESS) {\n /* If we aren't using EccKyber, use padding. */\n ret = GenerateKeys(ssh, hashId, !ssh->handshake->useEccKyber);\n }\n\n if (ret == WS_SUCCESS)\n ret = SendNewKeys(ssh);\n\n if (sigKeyBlock_ptr)\n WFREE(sigKeyBlock_ptr, ssh->ctx->heap, DYNTYPE_PRIVKEY);\n WLOG(WS_LOG_DEBUG, \"Leaving DoKexDhReply(), ret = %d\", ret);\n return ret;\n}\n\n\nstatic int DoNewKeys(WOLFSSH* ssh, byte* buf, word32 len, word32* idx)\n{\n int ret = WS_SUCCESS;\n\n WOLFSSH_UNUSED(buf);\n WOLFSSH_UNUSED(len);\n WOLFSSH_UNUSED(idx);\n\n if (ssh == NULL || ssh->handshake == NULL)\n ret = WS_BAD_ARGUMENT;\n\n if (ret == WS_SUCCESS) {\n ssh->peerEncryptId = ssh->handshake->encryptId;\n ssh->peerMacId = ssh->handshake->macId;\n ssh->peerBlockSz = ssh->handshake->blockSz;\n ssh->peerMacSz = ssh->handshake->macSz;\n ssh->peerAeadMode = ssh->handshake->aeadMode;\n WMEMCPY(&ssh->peerKeys, &ssh->handshake->peerKeys, sizeof(Keys));\n\n switch (ssh->peerEncryptId) {\n case ID_NONE:\n WLOG(WS_LOG_DEBUG, \"DNK: peer using cipher none\");\n", + "new": " ret = WS_INVALID_ALGO_ID;\n }\n }\n }\n FreePubKey(sigKeyBlock_ptr);\n }\n\n if (ret == WS_SUCCESS) {\n /* If we aren't using EccKyber, use padding. */\n ret = GenerateKeys(ssh, hashId, !ssh->handshake->useEccKyber);\n }\n\n if (ret == WS_SUCCESS) {\n ssh->handshake->expectMsgId = MSGID_NEWKEYS;\n WLOG_EXPECT_MSGID(ssh->handshake->expectMsgId);\n ret = SendNewKeys(ssh);\n }\n\n if (sigKeyBlock_ptr)\n WFREE(sigKeyBlock_ptr, ssh->ctx->heap, DYNTYPE_PRIVKEY);\n WLOG(WS_LOG_DEBUG, \"Leaving DoKexDhReply(), ret = %d\", ret);\n return ret;\n}\n\n\nstatic int DoNewKeys(WOLFSSH* ssh, byte* buf, word32 len, word32* idx)\n{\n int ret = WS_SUCCESS;\n\n WOLFSSH_UNUSED(buf);\n WOLFSSH_UNUSED(len);\n WOLFSSH_UNUSED(idx);\n\n if (ssh == NULL || ssh->handshake == NULL)\n ret = WS_BAD_ARGUMENT;\n\n if (ret == WS_SUCCESS) {\n if ((ssh->isKeying & WOLFSSH_SELF_IS_KEYING) ||\n !(ssh->isKeying & WOLFSSH_PEER_IS_KEYING)) {\n WLOG(WS_LOG_ERROR, \"Keying failed\");\n ret = WS_INVALID_STATE_E;\n }\n }\n\n if (ret == WS_SUCCESS) {\n ssh->peerEncryptId = ssh->handshake->encryptId;\n ssh->peerMacId = ssh->handshake->macId;\n ssh->peerBlockSz = ssh->handshake->blockSz;\n ssh->peerMacSz = ssh->handshake->macSz;\n ssh->peerAeadMode = ssh->handshake->aeadMode;\n WMEMCPY(&ssh->peerKeys, &ssh->handshake->peerKeys, sizeof(Keys));\n\n switch (ssh->peerEncryptId) {\n case ID_NONE:\n WLOG(WS_LOG_DEBUG, \"DNK: peer using cipher none\");\n" + }, + { + "old": " break;\n }\n\n if (ret == 0)\n ret = WS_SUCCESS;\n else\n ret = WS_CRYPTO_FAILED;\n }\n\n if (ret == WS_SUCCESS) {\n ssh->rxCount = 0;\n ssh->highwaterFlag = 0;\n ssh->isKeying = 0;\n HandshakeInfoFree(ssh->handshake, ssh->ctx->heap);\n ssh->handshake = NULL;\n WLOG(WS_LOG_DEBUG, \"Keying completed\");\n\n if (ssh->ctx->keyingCompletionCb)\n ssh->ctx->keyingCompletionCb(ssh->keyingCompletionCtx);\n }\n\n return ret;\n}\n\n\n#ifndef WOLFSSH_NO_DH_GEX_SHA256\nstatic int DoKexDhGexRequest(WOLFSSH* ssh,\n byte* buf, word32 len, word32* idx)\n{\n", + "new": " break;\n }\n\n if (ret == 0)\n ret = WS_SUCCESS;\n else\n ret = WS_CRYPTO_FAILED;\n }\n\n if (ret == WS_SUCCESS) {\n ssh->rxCount = 0;\n ssh->highwaterFlag = 0;\n\n /* Clear peer is keying flag */\n ssh->isKeying &= ~WOLFSSH_PEER_IS_KEYING;\n HandshakeInfoFree(ssh->handshake, ssh->ctx->heap);\n ssh->handshake = NULL;\n WLOG(WS_LOG_DEBUG, \"Keying completed\");\n if (ssh->ctx->keyingCompletionCb)\n ssh->ctx->keyingCompletionCb(ssh->keyingCompletionCtx);\n }\n\n return ret;\n}\n\n\n#ifndef WOLFSSH_NO_DH_GEX_SHA256\nstatic int DoKexDhGexRequest(WOLFSSH* ssh,\n byte* buf, word32 len, word32* idx)\n{\n" + }, + { + "old": "\n msg = buf[idx++];\n /* At this point, payload starts at \"buf + idx\". */\n\n /* sanity check on payloadSz. Uses \"or\" condition because of the case when\n * adding idx to payloadSz causes it to overflow.\n */\n if ((ssh->inputBuffer.bufferSz < payloadSz + idx) ||\n (payloadSz + idx < payloadSz)) {\n return WS_OVERFLOW_E;\n }\n\n if (!IsMessageAllowed(ssh, msg)) {\n return WS_MSGID_NOT_ALLOWED_E;\n }\n\n switch (msg) {\n\n case MSGID_DISCONNECT:\n WLOG(WS_LOG_DEBUG, \"Decoding MSGID_DISCONNECT\");\n ret = DoDisconnect(ssh, buf + idx, payloadSz, &payloadIdx);\n break;\n\n case MSGID_IGNORE:\n WLOG(WS_LOG_DEBUG, \"Decoding MSGID_IGNORE\");\n", + "new": "\n msg = buf[idx++];\n /* At this point, payload starts at \"buf + idx\". */\n\n /* sanity check on payloadSz. Uses \"or\" condition because of the case when\n * adding idx to payloadSz causes it to overflow.\n */\n if ((ssh->inputBuffer.bufferSz < payloadSz + idx) ||\n (payloadSz + idx < payloadSz)) {\n return WS_OVERFLOW_E;\n }\n\n if (!IsMessageAllowed(ssh, msg, WS_MSG_RECV)) {\n return WS_MSGID_NOT_ALLOWED_E;\n }\n\n switch (msg) {\n\n case MSGID_DISCONNECT:\n WLOG(WS_LOG_DEBUG, \"Decoding MSGID_DISCONNECT\");\n ret = DoDisconnect(ssh, buf + idx, payloadSz, &payloadIdx);\n break;\n\n case MSGID_IGNORE:\n WLOG(WS_LOG_DEBUG, \"Decoding MSGID_IGNORE\");\n" + }, + { + "old": " WLOG(WS_LOG_DEBUG, \"Decoding MSGID_DEBUG\");\n ret = DoDebug(ssh, buf + idx, payloadSz, &payloadIdx);\n break;\n\n case MSGID_EXT_INFO:\n WLOG(WS_LOG_DEBUG, \"Decoding MSGID_EXT_INFO\");\n ret = DoExtInfo(ssh, buf + idx, payloadSz, &payloadIdx);\n break;\n\n case MSGID_KEXINIT:\n WLOG(WS_LOG_DEBUG, \"Decoding MSGID_KEXINIT\");\n ret = DoKexInit(ssh, buf + idx, payloadSz, &payloadIdx);\n if (ssh->isKeying == 1 &&\n ssh->connectState == CONNECT_SERVER_CHANNEL_REQUEST_DONE) {\n if (ssh->handshake->kexId == ID_DH_GEX_SHA256) {\n#if !defined(WOLFSSH_NO_DH) && !defined(WOLFSSH_NO_DH_GEX_SHA256)\n ssh->error = SendKexDhGexRequest(ssh);\n#endif\n }\n else\n ssh->error = SendKexDhInit(ssh);\n }\n break;\n\n case MSGID_NEWKEYS:\n WLOG(WS_LOG_DEBUG, \"Decoding MSGID_NEWKEYS\");\n", + "new": " WLOG(WS_LOG_DEBUG, \"Decoding MSGID_DEBUG\");\n ret = DoDebug(ssh, buf + idx, payloadSz, &payloadIdx);\n break;\n\n case MSGID_EXT_INFO:\n WLOG(WS_LOG_DEBUG, \"Decoding MSGID_EXT_INFO\");\n ret = DoExtInfo(ssh, buf + idx, payloadSz, &payloadIdx);\n break;\n\n case MSGID_KEXINIT:\n WLOG(WS_LOG_DEBUG, \"Decoding MSGID_KEXINIT\");\n ret = DoKexInit(ssh, buf + idx, payloadSz, &payloadIdx);\n if ((ret == WS_SUCCESS || ret == WS_WANT_WRITE) &&\n ssh->handshake != NULL && ssh->isKeying &&\n ssh->connectState >= CONNECT_SERVER_CHANNEL_REQUEST_DONE) {\n if (ssh->handshake->kexId == ID_DH_GEX_SHA256) {\n#if !defined(WOLFSSH_NO_DH) && !defined(WOLFSSH_NO_DH_GEX_SHA256)\n ssh->error = SendKexDhGexRequest(ssh);\n#endif\n }\n else\n ssh->error = SendKexDhInit(ssh);\n }\n break;\n\n case MSGID_NEWKEYS:\n WLOG(WS_LOG_DEBUG, \"Decoding MSGID_NEWKEYS\");\n" + }, + { + "old": " WLOG(WS_LOG_DEBUG, \"Entering SendKexInit()\");\n\n if (ssh == NULL)\n ret = WS_BAD_ARGUMENT;\n\n if (ret == WS_SUCCESS && ssh->ctx->side == WOLFSSH_ENDPOINT_SERVER &&\n ssh->ctx->privateKeyCount == 0) {\n WLOG(WS_LOG_DEBUG, \"Server needs at least one private key\");\n ret = WS_BAD_ARGUMENT;\n }\n\n if (ret == WS_SUCCESS) {\n ssh->isKeying = 1;\n if (ssh->handshake == NULL) {\n ssh->handshake = HandshakeInfoNew(ssh->ctx->heap);\n if (ssh->handshake == NULL) {\n WLOG(WS_LOG_DEBUG, \"Couldn't allocate handshake info\");\n ret = WS_MEMORY_E;\n }\n }\n }\n\n if (ret == WS_SUCCESS) {\n if (!ssh->algoListKey && ssh->ctx->side == WOLFSSH_ENDPOINT_SERVER) {\n keyAlgoNamesSz = BuildNameList(NULL, 0,\n", + "new": " WLOG(WS_LOG_DEBUG, \"Entering SendKexInit()\");\n\n if (ssh == NULL)\n ret = WS_BAD_ARGUMENT;\n\n if (ret == WS_SUCCESS && ssh->ctx->side == WOLFSSH_ENDPOINT_SERVER &&\n ssh->ctx->privateKeyCount == 0) {\n WLOG(WS_LOG_DEBUG, \"Server needs at least one private key\");\n ret = WS_BAD_ARGUMENT;\n }\n\n if (ret == WS_SUCCESS) {\n /* Set self is keying flag since we started sending the KEX init msg */\n ssh->isKeying |= WOLFSSH_SELF_IS_KEYING;\n if (ssh->handshake == NULL) {\n ssh->handshake = HandshakeInfoNew(ssh->ctx->heap);\n if (ssh->handshake == NULL) {\n WLOG(WS_LOG_DEBUG, \"Couldn't allocate handshake info\");\n ret = WS_MEMORY_E;\n }\n }\n }\n\n if (ret == WS_SUCCESS) {\n if (!ssh->algoListKey && ssh->ctx->side == WOLFSSH_ENDPOINT_SERVER) {\n keyAlgoNamesSz = BuildNameList(NULL, 0,\n" + }, + { + "old": " if (ret > 0) {\n keyAlgoNamesSz = (word32)ret;\n ret = WS_SUCCESS;\n }\n }\n else {\n ret = WS_MEMORY_E;\n }\n }\n }\n\n if (ret == WS_SUCCESS) {\n if (ssh->ctx->side == WOLFSSH_ENDPOINT_CLIENT) {\n kexAlgoNamesPlus = \",ext-info-c\";\n kexAlgoNamesPlusSz = (word32)WSTRLEN(kexAlgoNamesPlus);\n }\n\n kexAlgoNamesSz = AlgoListSz(ssh->algoListKex);\n encAlgoNamesSz = AlgoListSz(ssh->algoListCipher);\n if (!keyAlgoNames) {\n keyAlgoNamesSz = AlgoListSz(ssh->algoListKey);\n }\n else {\n keyAlgoNamesSz = AlgoListSz(keyAlgoNames);\n }\n macAlgoNamesSz = AlgoListSz(ssh->algoListMac);\n noneNamesSz = AlgoListSz(cannedNoneNames);\n payloadSz = MSG_ID_SZ + COOKIE_SZ + (LENGTH_SZ * 11) + BOOLEAN_SZ +\n + kexAlgoNamesSz + kexAlgoNamesPlusSz + keyAlgoNamesSz\n", + "new": " if (ret > 0) {\n keyAlgoNamesSz = (word32)ret;\n ret = WS_SUCCESS;\n }\n }\n else {\n ret = WS_MEMORY_E;\n }\n }\n }\n\n if (ret == WS_SUCCESS) {\n kexAlgoNamesSz = AlgoListSz(ssh->algoListKex);\n encAlgoNamesSz = AlgoListSz(ssh->algoListCipher);\n if (!keyAlgoNames) {\n keyAlgoNamesSz = AlgoListSz(ssh->algoListKey);\n }\n else {\n keyAlgoNamesSz = AlgoListSz(keyAlgoNames);\n }\n macAlgoNamesSz = AlgoListSz(ssh->algoListMac);\n noneNamesSz = AlgoListSz(cannedNoneNames);\n payloadSz = MSG_ID_SZ + COOKIE_SZ + (LENGTH_SZ * 11) + BOOLEAN_SZ +\n + kexAlgoNamesSz + kexAlgoNamesPlusSz + keyAlgoNamesSz\n" + }, + { + "old": " }\n\n if (keyAlgoNames) {\n WFREE(keyAlgoNames, ssh->ctx->heap, DYNTYPE_STRING);\n }\n\n if (ret == WS_SUCCESS) {\n /* increase amount to be sent only if BundlePacket will be called */\n ssh->outputBuffer.length = idx;\n ret = BundlePacket(ssh);\n }\n\n if (ret == WS_SUCCESS)\n ret = wolfSSH_SendPacket(ssh);\n\n if (ret != WS_WANT_WRITE && ret != WS_SUCCESS)\n PurgePacket(ssh);\n\n WLOG(WS_LOG_DEBUG, \"Leaving SendKexInit(), ret = %d\", ret);\n return ret;\n}\n\n\nstruct wolfSSH_sigKeyBlockFull {\n byte pubKeyId; /* handshake->pubKeyId */\n byte pubKeyFmtId;\n", + "new": " }\n\n if (keyAlgoNames) {\n WFREE(keyAlgoNames, ssh->ctx->heap, DYNTYPE_STRING);\n }\n\n if (ret == WS_SUCCESS) {\n /* increase amount to be sent only if BundlePacket will be called */\n ssh->outputBuffer.length = idx;\n ret = BundlePacket(ssh);\n }\n\n if (ret == WS_SUCCESS) {\n ret = wolfSSH_SendPacket(ssh);\n }\n\n if (ret != WS_WANT_WRITE && ret != WS_SUCCESS)\n PurgePacket(ssh);\n\n WLOG(WS_LOG_DEBUG, \"Leaving SendKexInit(), ret = %d\", ret);\n return ret;\n}\n\n\nstruct wolfSSH_sigKeyBlockFull {\n byte pubKeyId; /* handshake->pubKeyId */\n byte pubKeyFmtId;\n" + }, + { + "old": " sigKeyBlock_ptr->pubKeyName, sigKeyBlock_ptr->pubKeyNameSz);\n idx += sigKeyBlock_ptr->pubKeyNameSz;\n c32toa(sigSz, output + idx);\n idx += LENGTH_SZ;\n WMEMCPY(output + idx, sig_ptr, sigSz);\n idx += sigSz;\n\n ssh->outputBuffer.length = idx;\n\n ret = BundlePacket(ssh);\n }\n\n if (ret == WS_SUCCESS)\n ret = SendNewKeys(ssh);\n\n if (ret == WS_SUCCESS && ssh->sendExtInfo) {\n ret = SendExtInfo(ssh);\n }\n\n if (ret != WS_WANT_WRITE && ret != WS_SUCCESS)\n PurgePacket(ssh);\n\n WLOG(WS_LOG_DEBUG, \"Leaving SendKexDhReply(), ret = %d\", ret);\n if (sigKeyBlock_ptr)\n WFREE(sigKeyBlock_ptr, heap, DYNTYPE_PRIVKEY);\n#ifdef WOLFSSH_SMALL_STACK\n if (f_ptr)\n WFREE(f_ptr, heap, DYNTYPE_BUFFER);\n if (sig_ptr)\n", + "new": " sigKeyBlock_ptr->pubKeyName, sigKeyBlock_ptr->pubKeyNameSz);\n idx += sigKeyBlock_ptr->pubKeyNameSz;\n c32toa(sigSz, output + idx);\n idx += LENGTH_SZ;\n WMEMCPY(output + idx, sig_ptr, sigSz);\n idx += sigSz;\n\n ssh->outputBuffer.length = idx;\n\n ret = BundlePacket(ssh);\n }\n\n if (ret == WS_SUCCESS) {\n ssh->handshake->expectMsgId = MSGID_NEWKEYS;\n WLOG_EXPECT_MSGID(ssh->handshake->expectMsgId);\n ret = SendNewKeys(ssh);\n }\n\n if (ret != WS_WANT_WRITE && ret != WS_SUCCESS)\n PurgePacket(ssh);\n\n WLOG(WS_LOG_DEBUG, \"Leaving SendKexDhReply(), ret = %d\", ret);\n if (sigKeyBlock_ptr)\n WFREE(sigKeyBlock_ptr, heap, DYNTYPE_PRIVKEY);\n#ifdef WOLFSSH_SMALL_STACK\n if (f_ptr)\n WFREE(f_ptr, heap, DYNTYPE_BUFFER);\n if (sig_ptr)\n" + }, + { + "old": "#endif\n\n default:\n WLOG(WS_LOG_DEBUG, \"SNK: using cipher invalid\");\n ret = WS_INVALID_ALGO_ID;\n }\n }\n\n if (ret == WS_SUCCESS) {\n ssh->txCount = 0;\n }\n\n if (ret == WS_SUCCESS)\n ret = wolfSSH_SendPacket(ssh);\n\n WLOG(WS_LOG_DEBUG, \"Leaving SendNewKeys(), ret = %d\", ret);\n return ret;\n}\n\n\n#ifndef WOLFSSH_NO_DH_GEX_SHA256\nint SendKexDhGexRequest(WOLFSSH* ssh)\n{\n byte* output;\n word32 idx = 0;\n word32 payloadSz;\n", + "new": "#endif\n\n default:\n WLOG(WS_LOG_DEBUG, \"SNK: using cipher invalid\");\n ret = WS_INVALID_ALGO_ID;\n }\n }\n\n if (ret == WS_SUCCESS) {\n ssh->txCount = 0;\n }\n\n if (ret == WS_SUCCESS) {\n ret = wolfSSH_SendPacket(ssh);\n\n /* Queued NEWKEYS is already bundled with the old keys. A partial\n * write is resumed by SendPacket, never by rebuilding NEWKEYS. */\n if (ret == WS_SUCCESS || ret == WS_WANT_WRITE)\n ssh->isKeying &= ~WOLFSSH_SELF_IS_KEYING;\n }\n\n WLOG(WS_LOG_DEBUG, \"Leaving SendNewKeys(), ret = %d\", ret);\n return ret;\n}\n\n\n#ifndef WOLFSSH_NO_DH_GEX_SHA256\nint SendKexDhGexRequest(WOLFSSH* ssh)\n{\n byte* output;\n word32 idx = 0;\n word32 payloadSz;\n" + }, + { + "old": " c32toa(ssh->handshake->dhGexMinSz, output + idx);\n idx += UINT32_SZ;\n c32toa(ssh->handshake->dhGexPreferredSz, output + idx);\n idx += UINT32_SZ;\n c32toa(ssh->handshake->dhGexMaxSz, output + idx);\n idx += UINT32_SZ;\n\n ssh->outputBuffer.length = idx;\n\n ret = BundlePacket(ssh);\n }\n\n if (ret == WS_SUCCESS)\n ret = wolfSSH_SendPacket(ssh);\n\n WLOG(WS_LOG_DEBUG, \"Leaving SendKexDhGexRequest(), ret = %d\", ret);\n return ret;\n}\n\n\nint SendKexDhGexGroup(WOLFSSH* ssh)\n{\n byte* output;\n word32 idx = 0;\n word32 payloadSz;\n const byte* primeGroup = dhPrimeGroup14;\n", + "new": " c32toa(ssh->handshake->dhGexMinSz, output + idx);\n idx += UINT32_SZ;\n c32toa(ssh->handshake->dhGexPreferredSz, output + idx);\n idx += UINT32_SZ;\n c32toa(ssh->handshake->dhGexMaxSz, output + idx);\n idx += UINT32_SZ;\n\n ssh->outputBuffer.length = idx;\n\n ret = BundlePacket(ssh);\n }\n\n if (ret == WS_SUCCESS) {\n WLOG_EXPECT_MSGID(MSGID_KEXDH_GEX_GROUP);\n ssh->handshake->expectMsgId = MSGID_KEXDH_GEX_GROUP;\n ret = wolfSSH_SendPacket(ssh);\n }\n\n WLOG(WS_LOG_DEBUG, \"Leaving SendKexDhGexRequest(), ret = %d\", ret);\n return ret;\n}\n\n\nint SendKexDhGexGroup(WOLFSSH* ssh)\n{\n byte* output;\n word32 idx = 0;\n word32 payloadSz;\n const byte* primeGroup = dhPrimeGroup14;\n" + }, + { + "old": "{\n byte* output;\n word32 idx = 0;\n word32 payloadSz;\n#ifndef WOLFSSH_NO_DH\n const byte* primeGroup = NULL;\n word32 primeGroupSz = 0;\n const byte* generator = NULL;\n word32 generatorSz = 0;\n#endif\n int ret = WS_SUCCESS;\n byte msgId = MSGID_KEXDH_INIT;\n byte e[MAX_KEX_KEY_SZ+1]; /* plus 1 in case of padding. */\n word32 eSz = (word32)sizeof(e);\n byte ePad = 0;\n\n WLOG(WS_LOG_DEBUG, \"Entering SendKexDhInit()\");\n\n switch (ssh->handshake->kexId) {\n#ifndef WOLFSSH_NO_DH_GROUP1_SHA1\n case ID_DH_GROUP1_SHA1:\n ssh->handshake->useDh = 1;\n primeGroup = dhPrimeGroup1;\n primeGroupSz = dhPrimeGroup1Sz;\n", + "new": "{\n byte* output;\n word32 idx = 0;\n word32 payloadSz;\n#ifndef WOLFSSH_NO_DH\n const byte* primeGroup = NULL;\n word32 primeGroupSz = 0;\n const byte* generator = NULL;\n word32 generatorSz = 0;\n#endif\n int ret = WS_SUCCESS;\n byte msgId = MSGID_KEXDH_INIT;\n byte expectMsgId = MSGID_KEXDH_REPLY;\n byte e[MAX_KEX_KEY_SZ+1]; /* plus 1 in case of padding. */\n word32 eSz = (word32)sizeof(e);\n byte ePad = 0;\n\n WLOG(WS_LOG_DEBUG, \"Entering SendKexDhInit()\");\n\n switch (ssh->handshake->kexId) {\n#ifndef WOLFSSH_NO_DH_GROUP1_SHA1\n case ID_DH_GROUP1_SHA1:\n ssh->handshake->useDh = 1;\n primeGroup = dhPrimeGroup1;\n primeGroupSz = dhPrimeGroup1Sz;\n" + }, + { + "old": " generator = dhGenerator;\n generatorSz = dhGeneratorSz;\n break;\n#endif\n#ifndef WOLFSSH_NO_DH_GEX_SHA256\n case ID_DH_GEX_SHA256:\n ssh->handshake->useDh = 1;\n primeGroup = ssh->handshake->primeGroup;\n primeGroupSz = ssh->handshake->primeGroupSz;\n generator = ssh->handshake->generator;\n generatorSz = ssh->handshake->generatorSz;\n msgId = MSGID_KEXDH_GEX_INIT;\n break;\n#endif\n#ifndef WOLFSSH_NO_ECDH_SHA2_NISTP256\n case ID_ECDH_SHA2_NISTP256:\n ssh->handshake->useEcc = 1;\n msgId = MSGID_KEXECDH_INIT;\n break;\n#endif\n#ifndef WOLFSSH_NO_ECDH_SHA2_NISTP384\n case ID_ECDH_SHA2_NISTP384:\n ssh->handshake->useEcc = 1;\n msgId = MSGID_KEXECDH_INIT;\n", + "new": " generator = dhGenerator;\n generatorSz = dhGeneratorSz;\n break;\n#endif\n#ifndef WOLFSSH_NO_DH_GEX_SHA256\n case ID_DH_GEX_SHA256:\n ssh->handshake->useDh = 1;\n primeGroup = ssh->handshake->primeGroup;\n primeGroupSz = ssh->handshake->primeGroupSz;\n generator = ssh->handshake->generator;\n generatorSz = ssh->handshake->generatorSz;\n msgId = MSGID_KEXDH_GEX_INIT;\n expectMsgId = MSGID_KEXDH_GEX_REPLY;\n break;\n#endif\n#ifndef WOLFSSH_NO_ECDH_SHA2_NISTP256\n case ID_ECDH_SHA2_NISTP256:\n ssh->handshake->useEcc = 1;\n msgId = MSGID_KEXECDH_INIT;\n break;\n#endif\n#ifndef WOLFSSH_NO_ECDH_SHA2_NISTP384\n case ID_ECDH_SHA2_NISTP384:\n ssh->handshake->useEcc = 1;\n msgId = MSGID_KEXECDH_INIT;\n" + }, + { + "old": " output[idx] = 0;\n idx++;\n }\n\n WMEMCPY(output + idx, e, eSz);\n idx += eSz;\n\n ssh->outputBuffer.length = idx;\n\n ret = BundlePacket(ssh);\n }\n\n if (ret == WS_SUCCESS)\n ret = wolfSSH_SendPacket(ssh);\n\n WLOG(WS_LOG_DEBUG, \"Leaving SendKexDhInit(), ret = %d\", ret);\n return ret;\n}\n\n\nint SendUnimplemented(WOLFSSH* ssh)\n{\n byte* output;\n word32 idx = 0;\n int ret = WS_SUCCESS;\n\n", + "new": " output[idx] = 0;\n idx++;\n }\n\n WMEMCPY(output + idx, e, eSz);\n idx += eSz;\n\n ssh->outputBuffer.length = idx;\n\n ret = BundlePacket(ssh);\n }\n\n if (ret == WS_SUCCESS) {\n WLOG_EXPECT_MSGID(expectMsgId);\n ssh->handshake->expectMsgId = expectMsgId;\n ret = wolfSSH_SendPacket(ssh);\n }\n\n WLOG(WS_LOG_DEBUG, \"Leaving SendKexDhInit(), ret = %d\", ret);\n return ret;\n}\n\n\nint SendUnimplemented(WOLFSSH* ssh)\n{\n byte* output;\n word32 idx = 0;\n int ret = WS_SUCCESS;\n\n" + }, + { + "old": " ret = SendUserAuthBanner(ssh);\n\n return ret;\n}\n\n\n#define WS_EXTINFO_EXTENSION_COUNT 1\nstatic const char serverSigAlgsName[] = \"server-sig-algs\";\n\n\nint SendExtInfo(WOLFSSH* ssh)\n{\n byte* output;\n word32 idx;\n word32 keyAlgoNamesSz = 0;\n word32 serverSigAlgsNameSz = 0;\n int ret = WS_SUCCESS;\n\n WLOG(WS_LOG_DEBUG, \"Entering SendExtInfo()\");\n\n if (ssh == NULL) {\n ret = WS_BAD_ARGUMENT;\n }\n\n if (ret == WS_SUCCESS) {\n keyAlgoNamesSz = AlgoListSz(ssh->algoListKeyAccepted);\n serverSigAlgsNameSz = AlgoListSz(serverSigAlgsName);\n ret = PreparePacket(ssh, MSG_ID_SZ + UINT32_SZ + (LENGTH_SZ * 2)\n + serverSigAlgsNameSz + keyAlgoNamesSz);\n }\n\n if (ret == WS_SUCCESS) {\n output = ssh->outputBuffer.buffer;\n idx = ssh->outputBuffer.length;\n\n output[idx++] = MSGID_EXT_INFO;\n c32toa(WS_EXTINFO_EXTENSION_COUNT, output + idx);\n idx += UINT32_SZ;\n\n c32toa(serverSigAlgsNameSz, output + idx);\n idx += LENGTH_SZ;\n WMEMCPY(output + idx, serverSigAlgsName, serverSigAlgsNameSz);\n idx += serverSigAlgsNameSz;\n\n c32toa(keyAlgoNamesSz, output + idx);\n idx += LENGTH_SZ;\n WMEMCPY(output + idx, ssh->algoListKeyAccepted, keyAlgoNamesSz);\n idx += keyAlgoNamesSz;\n\n ssh->outputBuffer.length = idx;\n\n ret = BundlePacket(ssh);\n }\n\n if (ret == WS_SUCCESS) {\n ret = wolfSSH_SendPacket(ssh);\n }\n\n WLOG(WS_LOG_DEBUG, \"Leaving SendExtInfo(), ret = %d\", ret);\n return ret;\n}\n\n\n/* Updates the payload size, and maybe loads keys. */\nstatic int PrepareUserAuthRequestPassword(WOLFSSH* ssh, word32* payloadSz,\n const WS_UserAuthData* authData)\n{\n int ret = WS_SUCCESS;\n\n if (ssh == NULL || payloadSz == NULL || authData == NULL)\n ret = WS_BAD_ARGUMENT;\n\n", + "new": " ret = SendUserAuthBanner(ssh);\n\n return ret;\n}\n\n\n#define WS_EXTINFO_EXTENSION_COUNT 1\nstatic const char serverSigAlgsName[] = \"server-sig-algs\";\n\n\nint SendExtInfo(WOLFSSH* ssh)\n{\n WOLFSSH_UNUSED(ssh);\n return WS_NOT_COMPILED;\n}\n\n\n/* Updates the payload size, and maybe loads keys. */\nstatic int PrepareUserAuthRequestPassword(WOLFSSH* ssh, word32* payloadSz,\n const WS_UserAuthData* authData)\n{\n int ret = WS_SUCCESS;\n\n if (ssh == NULL || payloadSz == NULL || authData == NULL)\n ret = WS_BAD_ARGUMENT;\n\n" + }, + { + "old": "int SendChannelEof(WOLFSSH* ssh, word32 peerChannelId)\n{\n byte* output;\n word32 idx;\n int ret = WS_SUCCESS;\n WOLFSSH_CHANNEL* channel = NULL;\n\n WLOG(WS_LOG_DEBUG, \"Entering SendChannelEof()\");\n\n if (ssh == NULL)\n ret = WS_BAD_ARGUMENT;", + "new": "int SendChannelEof(WOLFSSH* ssh, word32 peerChannelId)\n{\n byte* output;\n word32 idx;\n int ret = WS_SUCCESS;\n WOLFSSH_CHANNEL* channel = NULL;\n\n WLOG(WS_LOG_DEBUG, \"Entering SendChannelEof()\");\n\n if (ssh == NULL)\n ret = WS_BAD_ARGUMENT;\n\n if (ret == WS_SUCCESS) {\n if (!IsMessageAllowed(ssh, MSGID_CHANNEL_EOF, WS_MSG_SEND)) {\n ret = WS_MSGID_NOT_ALLOWED_E;\n }\n }" + }, + { + "old": " const char* str = \"exit-status\";\n word32 idx;\n word32 strSz = 0;\n int ret = WS_SUCCESS;\n WOLFSSH_CHANNEL* channel = NULL;\n\n WLOG(WS_LOG_DEBUG, \"Entering SendChannelExit(), status = %d\", status);\n\n if (ssh == NULL)\n ret = WS_BAD_ARGUMENT;\n\n if (ret == WS_SUCCESS) {\n channel = ChannelFind(ssh, peerChannelId, WS_CHANNEL_ID_PEER);\n if (channel == NULL)\n ret = WS_INVALID_CHANID;\n }\n\n if (ret == WS_SUCCESS) {\n strSz = (word32)WSTRLEN(str);\n ret = PreparePacket(ssh, MSG_ID_SZ + UINT32_SZ + LENGTH_SZ + strSz +\n BOOLEAN_SZ + UINT32_SZ);\n }\n\n if (ret == WS_SUCCESS) {\n", + "new": " const char* str = \"exit-status\";\n word32 idx;\n word32 strSz = 0;\n int ret = WS_SUCCESS;\n WOLFSSH_CHANNEL* channel = NULL;\n\n WLOG(WS_LOG_DEBUG, \"Entering SendChannelExit(), status = %d\", status);\n\n if (ssh == NULL)\n ret = WS_BAD_ARGUMENT;\n\n if (ret == WS_SUCCESS) {\n if (!IsMessageAllowed(ssh, MSGID_CHANNEL_REQUEST, WS_MSG_SEND)) {\n ret = WS_MSGID_NOT_ALLOWED_E;\n }\n }\n\n if (ret == WS_SUCCESS) {\n channel = ChannelFind(ssh, peerChannelId, WS_CHANNEL_ID_PEER);\n if (channel == NULL)\n ret = WS_INVALID_CHANID;\n }\n\n if (ret == WS_SUCCESS) {\n strSz = (word32)WSTRLEN(str);\n ret = PreparePacket(ssh, MSG_ID_SZ + UINT32_SZ + LENGTH_SZ + strSz +\n BOOLEAN_SZ + UINT32_SZ);\n }\n\n if (ret == WS_SUCCESS) {\n" + }, + { + "old": "int SendChannelWindowAdjust(WOLFSSH* ssh, word32 channelId,\n word32 bytesToAdd)\n{\n byte* output;\n word32 idx;\n int ret = WS_SUCCESS;\n WOLFSSH_CHANNEL* channel;\n\n WLOG(WS_LOG_DEBUG, \"Entering SendChannelWindowAdjust()\");\n\n if (ssh == NULL)\n ret = WS_BAD_ARGUMENT;\n\n channel = ChannelFind(ssh, channelId, WS_CHANNEL_ID_SELF);\n if (channel == NULL) {\n WLOG(WS_LOG_DEBUG, \"Invalid channel\");\n ret = WS_INVALID_CHANID;\n }\n if (ret == WS_SUCCESS)\n ret = PreparePacket(ssh, MSG_ID_SZ + (UINT32_SZ * 2));\n\n if (ret == WS_SUCCESS) {\n output = ssh->outputBuffer.buffer;\n idx = ssh->outputBuffer.length;\n", + "new": "int SendChannelWindowAdjust(WOLFSSH* ssh, word32 channelId,\n word32 bytesToAdd)\n{\n byte* output;\n word32 idx;\n int ret = WS_SUCCESS;\n WOLFSSH_CHANNEL* channel;\n\n WLOG(WS_LOG_DEBUG, \"Entering SendChannelWindowAdjust()\");\n\n if (ssh == NULL)\n ret = WS_BAD_ARGUMENT;\n\n if (ret == WS_SUCCESS) {\n if (!IsMessageAllowed(ssh, MSGID_CHANNEL_WINDOW_ADJUST, WS_MSG_SEND)) {\n ret = WS_MSGID_NOT_ALLOWED_E;\n }\n }\n\n channel = ChannelFind(ssh, channelId, WS_CHANNEL_ID_SELF);\n if (channel == NULL) {\n WLOG(WS_LOG_DEBUG, \"Invalid channel\");\n ret = WS_INVALID_CHANID;\n }\n if (ret == WS_SUCCESS)\n ret = PreparePacket(ssh, MSG_ID_SZ + (UINT32_SZ * 2));\n\n if (ret == WS_SUCCESS) {\n output = ssh->outputBuffer.buffer;\n idx = ssh->outputBuffer.length;\n" + }, + { + "old": "static const char serverSigAlgsName[] = \"server-sig-algs\";\n", + "new": "" + } + ] + }, + "src/ssh.c": { + "sha256": "a4f479ff87eea0980ec1ebdf2c7dd090da473780181b695a56799cb9611f4366", + "edits": [ + { + "old": " WOLFSSH_BUFFER* inputBuffer;\n\n WLOG(WS_LOG_DEBUG, \"Entering wolfSSH_stream_read()\");\n\n if (ssh == NULL || buf == NULL || bufSz == 0 || ssh->channelList == NULL)\n return WS_BAD_ARGUMENT;\n\n if (ssh->channelList->eofRxd) {\n ssh->error = WS_EOF;\n return WS_ERROR;\n }\n\n inputBuffer = &ssh->channelList->inputBuffer;\n ssh->error = WS_SUCCESS;\n\n if (ret == WS_SUCCESS) {\n WLOG(WS_LOG_DEBUG, \" Stream read index of %u\", inputBuffer->idx);\n WLOG(WS_LOG_DEBUG, \" Stream read ava data %u\", inputBuffer->length);\n while (inputBuffer->length - inputBuffer->idx == 0) {\n WLOG(WS_LOG_DEBUG,\n \"Starting to recieve data at current index of %u\",\n inputBuffer->idx);\n ret = DoReceive(ssh);\n if (ssh->channelList == NULL || ssh->channelList->eofRxd)\n", + "new": " WOLFSSH_BUFFER* inputBuffer;\n\n WLOG(WS_LOG_DEBUG, \"Entering wolfSSH_stream_read()\");\n\n if (ssh == NULL || buf == NULL || bufSz == 0 || ssh->channelList == NULL)\n return WS_BAD_ARGUMENT;\n\n if (ssh->channelList->eofRxd) {\n ssh->error = WS_EOF;\n return WS_ERROR;\n }\n\n if (ssh->isKeying) {\n ssh->error = WS_REKEYING;\n return WS_FATAL_ERROR;\n }\n\n inputBuffer = &ssh->channelList->inputBuffer;\n ssh->error = WS_SUCCESS;\n\n if (ret == WS_SUCCESS) {\n WLOG(WS_LOG_DEBUG, \" Stream read index of %u\", inputBuffer->idx);\n WLOG(WS_LOG_DEBUG, \" Stream read ava data %u\", inputBuffer->length);\n while (inputBuffer->length - inputBuffer->idx == 0) {\n WLOG(WS_LOG_DEBUG,\n \"Starting to recieve data at current index of %u\",\n inputBuffer->idx);\n ret = DoReceive(ssh);\n if (ssh->channelList == NULL || ssh->channelList->eofRxd)\n" + }, + { + "old": " if (ssh->lastRxId != ssh->channelList->channel) {\n ret = WS_ERROR;\n break;\n }\n else {\n ret = WS_SUCCESS;\n }\n }\n }\n }\n\n /* update internal input buffer based on data read */\n if (ret == WS_SUCCESS) {\n int n;\n\n n = min(bufSz, inputBuffer->length - inputBuffer->idx);\n if (n <= 0)\n ret = WS_BUFFER_E;\n else {\n WMEMCPY(buf, inputBuffer->buffer + inputBuffer->idx, n);\n ret = _UpdateChannelWindow(ssh->channelList);\n if (ret == WS_SUCCESS) {\n inputBuffer->idx += n;\n ret = n;\n }\n", + "new": " if (ssh->lastRxId != ssh->channelList->channel) {\n ret = WS_ERROR;\n break;\n }\n else {\n ret = WS_SUCCESS;\n }\n }\n }\n }\n\n /* update internal input buffer based on data read */\n if (ret == WS_SUCCESS && !ssh->isKeying) {\n int n;\n\n n = min(bufSz, inputBuffer->length - inputBuffer->idx);\n if (n <= 0)\n ret = WS_BUFFER_E;\n else {\n WMEMCPY(buf, inputBuffer->buffer + inputBuffer->idx, n);\n ret = _UpdateChannelWindow(ssh->channelList);\n if (ret == WS_SUCCESS) {\n inputBuffer->idx += n;\n ret = n;\n }\n" + }, + { + "old": "\nint wolfSSH_stream_send(WOLFSSH* ssh, byte* buf, word32 bufSz)\n{\n int bytesTxd = 0;\n\n WLOG(WS_LOG_DEBUG, \"Entering wolfSSH_stream_send()\");\n\n if (ssh == NULL || buf == NULL || ssh->channelList == NULL)\n return WS_BAD_ARGUMENT;\n\n if (ssh->isKeying) {\n ssh->error = WS_REKEYING;\n return WS_REKEYING;\n }\n\n bytesTxd = SendChannelData(ssh, ssh->channelList->channel, buf, bufSz);\n\n WLOG(WS_LOG_DEBUG, \"Leaving wolfSSH_stream_send(), txd = %d\", bytesTxd);\n return bytesTxd;\n}\n\n\nint wolfSSH_ChannelIdSend(WOLFSSH* ssh, word32 channelId,\n byte* buf, word32 bufSz)\n{\n", + "new": "\nint wolfSSH_stream_send(WOLFSSH* ssh, byte* buf, word32 bufSz)\n{\n int bytesTxd = 0;\n\n WLOG(WS_LOG_DEBUG, \"Entering wolfSSH_stream_send()\");\n\n if (ssh == NULL || buf == NULL || ssh->channelList == NULL)\n return WS_BAD_ARGUMENT;\n\n if (ssh->isKeying) {\n ssh->error = WS_REKEYING;\n return WS_FATAL_ERROR;\n }\n\n bytesTxd = SendChannelData(ssh, ssh->channelList->channel, buf, bufSz);\n\n WLOG(WS_LOG_DEBUG, \"Leaving wolfSSH_stream_send(), txd = %d\", bytesTxd);\n return bytesTxd;\n}\n\n\nint wolfSSH_ChannelIdSend(WOLFSSH* ssh, word32 channelId,\n byte* buf, word32 bufSz)\n{\n" + }, + { + "old": " return bufSz;\n}\n\n\nint wolfSSH_ChannelRead(WOLFSSH_CHANNEL* channel, byte* buf, word32 bufSz)\n{\n\n WLOG(WS_LOG_DEBUG, \"Entering wolfSSH_ChannelRead()\");\n\n if (channel == NULL || buf == NULL || bufSz == 0)\n return WS_BAD_ARGUMENT;\n\n bufSz = _ChannelRead(channel, buf, bufSz);\n\n WLOG(WS_LOG_DEBUG, \"Leaving wolfSSH_ChannelRead(), bytesRxd = %d\",\n bufSz);\n return bufSz;\n}\n\n\nint wolfSSH_ChannelSend(WOLFSSH_CHANNEL* channel,\n const byte* buf, word32 bufSz)\n{\n int bytesTxd = 0;\n", + "new": " return bufSz;\n}\n\n\nint wolfSSH_ChannelRead(WOLFSSH_CHANNEL* channel, byte* buf, word32 bufSz)\n{\n\n WLOG(WS_LOG_DEBUG, \"Entering wolfSSH_ChannelRead()\");\n\n if (channel == NULL || buf == NULL || bufSz == 0)\n return WS_BAD_ARGUMENT;\n\n if (channel->ssh->isKeying) {\n channel->ssh->error = WS_REKEYING;\n return WS_REKEYING;\n }\n\n bufSz = _ChannelRead(channel, buf, bufSz);\n\n WLOG(WS_LOG_DEBUG, \"Leaving wolfSSH_ChannelRead(), bytesRxd = %d\",\n bufSz);\n return bufSz;\n}\n\n\nint wolfSSH_ChannelSend(WOLFSSH_CHANNEL* channel,\n const byte* buf, word32 bufSz)\n{\n int bytesTxd = 0;\n" + } + ] + }, + "wolfssh/internal.h": { + "sha256": "8e417149a68f8a6c0506957adf014b3e6c1727a723536826ce5fb0c9e1f1aba3", + "edits": [ + { + "old": " #define WOLFSSH_MAX_FILE_SIZE (1024ul * 1024ul * 4)\n#endif\n#ifndef WOLFSSH_MAX_PVT_KEYS\n #define WOLFSSH_MAX_PVT_KEYS 8\n#endif\n#ifndef WOLFSSH_MAX_PUB_KEY_ALGO\n #define WOLFSSH_MAX_PUB_KEY_ALGO (WOLFSSH_MAX_PVT_KEYS + 2)\n#endif\n#ifndef WOLFSSH_KEY_QUANTITY_REQ\n #define WOLFSSH_KEY_QUANTITY_REQ 1\n#endif\n\n\nWOLFSSH_LOCAL byte NameToId(const char* name, word32 nameSz);\nWOLFSSH_LOCAL const char* IdToName(byte id);\nWOLFSSH_LOCAL const char* NameByIndexType(byte type, word32* index);\n\n\n/* For cases when openssl coexist is used */\n#ifdef WC_NO_COMPAT_AES_BLOCK_SIZE\n #define AES_BLOCK_SIZE WC_AES_BLOCK_SIZE\n#endif\n\n#define STATIC_BUFFER_LEN AES_BLOCK_SIZE\n", + "new": " #define WOLFSSH_MAX_FILE_SIZE (1024ul * 1024ul * 4)\n#endif\n#ifndef WOLFSSH_MAX_PVT_KEYS\n #define WOLFSSH_MAX_PVT_KEYS 8\n#endif\n#ifndef WOLFSSH_MAX_PUB_KEY_ALGO\n #define WOLFSSH_MAX_PUB_KEY_ALGO (WOLFSSH_MAX_PVT_KEYS + 2)\n#endif\n#ifndef WOLFSSH_KEY_QUANTITY_REQ\n #define WOLFSSH_KEY_QUANTITY_REQ 1\n#endif\n\n/* Keep track of keying state for both sides of the connection.\n * WOLFSSH_SELF_IS_KEYING gets set on sending KEX init and\n * WOLFSSH_PEER_IS_KEYING gets set on receiving KEX init */\n#define WOLFSSH_PEER_IS_KEYING 0x01\n#define WOLFSSH_SELF_IS_KEYING 0x02\n\nWOLFSSH_LOCAL byte NameToId(const char* name, word32 nameSz);\nWOLFSSH_LOCAL const char* IdToName(byte id);\nWOLFSSH_LOCAL const char* NameByIndexType(byte type, word32* index);\n\n\n/* For cases when openssl coexist is used */\n#ifdef WC_NO_COMPAT_AES_BLOCK_SIZE\n #define AES_BLOCK_SIZE WC_AES_BLOCK_SIZE\n#endif\n\n#define STATIC_BUFFER_LEN AES_BLOCK_SIZE\n" + }, + { + "old": "\ntypedef struct Keys {\n byte iv[AES_BLOCK_SIZE];\n byte ivSz;\n byte encKey[AES_256_KEY_SIZE];\n byte encKeySz;\n byte macKey[MAX_HMAC_SZ];\n byte macKeySz;\n} Keys;\n\n\ntypedef struct HandshakeInfo {\n byte kexId;\n byte kexIdGuess;\n byte kexHashId;\n byte pubKeyId;\n byte encryptId;\n byte macId;\n byte kexPacketFollows;\n byte aeadMode;\n\n byte blockSz;\n byte macSz;\n\n", + "new": "\ntypedef struct Keys {\n byte iv[AES_BLOCK_SIZE];\n byte ivSz;\n byte encKey[AES_256_KEY_SIZE];\n byte encKeySz;\n byte macKey[MAX_HMAC_SZ];\n byte macKeySz;\n} Keys;\n\n\ntypedef struct HandshakeInfo {\n byte expectMsgId;\n byte kexId;\n byte kexIdGuess;\n byte kexHashId;\n byte pubKeyId;\n byte encryptId;\n byte macId;\n byte kexPacketFollows;\n byte aeadMode;\n\n byte blockSz;\n byte macSz;\n\n" + }, + { + "old": " char* userName;\n word32 userNameSz;\n char* password;\n word32 passwordSz;\n byte* pkBlob;\n word32 pkBlobSz;\n byte* peerProtoId; /* Save for rekey */\n word32 peerProtoIdSz;\n void* publicKeyCheckCtx;\n byte sendTerminalRequest;\n byte userAuthPkDone;\n byte sendExtInfo;\n byte* peerSigId;\n word32 peerSigIdSz;\n\n#ifdef USE_WINDOWS_API\n word32 defaultAttr; /* default windows attributes */\n byte defaultAttrSet;\n byte escBuf[WOLFSSL_MAX_ESCBUF]; /* console codes are about 3 byte and\n * have max arguments of 16 */\n byte escBufSz;\n byte escState; /* current console translation state */\n#endif\n#ifdef WOLFSSH_SFTP\n", + "new": " char* userName;\n word32 userNameSz;\n char* password;\n word32 passwordSz;\n byte* pkBlob;\n word32 pkBlobSz;\n byte* peerProtoId; /* Save for rekey */\n word32 peerProtoIdSz;\n void* publicKeyCheckCtx;\n byte sendTerminalRequest;\n byte userAuthPkDone;\n byte sendExtInfo;\n byte extInfoSent; /* track if the ext info has already been sent */\n byte* peerSigId;\n word32 peerSigIdSz;\n\n#ifdef USE_WINDOWS_API\n word32 defaultAttr; /* default windows attributes */\n byte defaultAttrSet;\n byte escBuf[WOLFSSL_MAX_ESCBUF]; /* console codes are about 3 byte and\n * have max arguments of 16 */\n byte escBufSz;\n byte escState; /* current console translation state */\n#endif\n#ifdef WOLFSSH_SFTP\n" + }, + { + "old": "};\n\n\nenum ProcessReplyStates {\n PROCESS_INIT,\n PROCESS_PACKET_LENGTH,\n PROCESS_PACKET_FINISH,\n PROCESS_PACKET\n};\n\n\nenum WS_MessageIds {\n MSGID_DISCONNECT = 1,\n MSGID_IGNORE = 2,\n MSGID_UNIMPLEMENTED = 3,\n MSGID_DEBUG = 4,\n MSGID_SERVICE_REQUEST = 5,\n MSGID_SERVICE_ACCEPT = 6,\n MSGID_EXT_INFO = 7,\n\n MSGID_KEXINIT = 20,\n MSGID_NEWKEYS = 21,\n\n MSGID_KEXDH_INIT = 30,\n", + "new": "};\n\n\nenum ProcessReplyStates {\n PROCESS_INIT,\n PROCESS_PACKET_LENGTH,\n PROCESS_PACKET_FINISH,\n PROCESS_PACKET\n};\n\n\nenum WS_MessageIds {\n MSGID_NONE = 0,\n\n MSGID_DISCONNECT = 1,\n MSGID_IGNORE = 2,\n MSGID_UNIMPLEMENTED = 3,\n MSGID_DEBUG = 4,\n MSGID_SERVICE_REQUEST = 5,\n MSGID_SERVICE_ACCEPT = 6,\n MSGID_EXT_INFO = 7,\n\n MSGID_KEXINIT = 20,\n MSGID_NEWKEYS = 21,\n\n MSGID_KEXDH_INIT = 30,\n" + }, + { + "old": " MSGID_CHANNEL_OPEN_FAIL = 92,\n MSGID_CHANNEL_WINDOW_ADJUST = 93,\n MSGID_CHANNEL_DATA = 94,\n MSGID_CHANNEL_EXTENDED_DATA = 95,\n MSGID_CHANNEL_EOF = 96,\n MSGID_CHANNEL_CLOSE = 97,\n MSGID_CHANNEL_REQUEST = 98,\n MSGID_CHANNEL_SUCCESS = 99,\n MSGID_CHANNEL_FAILURE = 100\n};\n\n\n/* Allows the server to receive up to KEXDH GEX Request during KEX. */\n#define MSGID_KEXDH_LIMIT MSGID_KEXDH_GEX_REQUEST\n\n/* The endpoints should not allow message IDs greater than or\n * equal to msgid 80 before user authentication is complete.\n * Per RFC 4252 section 6. */\n#define MSGID_USERAUTH_LIMIT 80\n\n/* The client should only send the user auth request message\n * (50), it should not accept it. The server should only receive\n * the user auth request message, it should not accept the other\n * user auth messages, it sends them. (>50) */\n#define MSGID_USERAUTH_RESTRICT 50\n\n\n#define CHANNEL_EXTENDED_DATA_STDERR WOLFSSH_EXT_DATA_STDERR\n\n\n/* dynamic memory types */\nenum WS_DynamicTypes {\n DYNTYPE_STRING = 500,\n DYNTYPE_CTX,\n DYNTYPE_SSH,\n DYNTYPE_CHANNEL,\n DYNTYPE_BUFFER,\n DYNTYPE_ID,\n DYNTYPE_HS,\n DYNTYPE_CA,\n DYNTYPE_CERT,\n", + "new": " MSGID_CHANNEL_OPEN_FAIL = 92,\n MSGID_CHANNEL_WINDOW_ADJUST = 93,\n MSGID_CHANNEL_DATA = 94,\n MSGID_CHANNEL_EXTENDED_DATA = 95,\n MSGID_CHANNEL_EOF = 96,\n MSGID_CHANNEL_CLOSE = 97,\n MSGID_CHANNEL_REQUEST = 98,\n MSGID_CHANNEL_SUCCESS = 99,\n MSGID_CHANNEL_FAILURE = 100\n};\n\n\n/* The following message ID ranges are described in RFC 5251, section 7. */\nenum WS_MessageIdLimits {\n/* Transport Layer Protocol: */\n MSGIDLIMIT_TRANS_MIN = 1,\n MSGIDLIMIT_TRANS_GEN_MIN = 1,\n MSGIDLIMIT_TRANS_GEN_MAX = 19,\n MSGIDLIMIT_TRANS_ALGO_MIN = 20,\n MSGIDLIMIT_TRANS_ALGO_MAX = 29,\n MSGIDLIMIT_TRANS_KEX_MIN = 30,\n MSGIDLIMIT_TRANS_KEX_MAX = 49,\n MSGIDLIMIT_TRANS_MAX = 49,\n/* User Authentication Protocol: */\n MSGIDLIMIT_AUTH_MIN = 50,\n MSGIDLIMIT_AUTH_GEN_MIN = 50,\n MSGIDLIMIT_AUTH_GEN_MAX = 59,\n MSGIDLIMIT_AUTH_METH_MIN = 60,\n MSGIDLIMIT_AUTH_METH_MAX = 79,\n MSGIDLIMIT_AUTH_MAX = 79,\n/* Connection Protocol: */\n MSGIDLIMIT_CONN_MIN = 80,\n MSGIDLIMIT_CONN_GEN_MIN = 80,\n MSGIDLIMIT_CONN_GEN_MAX = 89,\n MSGIDLIMIT_CONN_CHAN_MIN = 90,\n MSGIDLIMIT_CONN_CHAN_MAX = 127,\n MSGIDLIMIT_CONN_MAX = 127,\n/* Reserved For Client Protocols: */\n MSGIDLIMIT_RESERVED_MIN = 128,\n MSGIDLIMIT_RESERVED_MAX = 191,\n/* Local Extensions: */\n MSGIDLIMIT_EXTENDED_MIN = 192,\n MSGIDLIMIT_EXTENDED_MAX = 255,\n};\n\n/* Message ID bounds checking. */\n#define MSGIDLIMIT_BOUND(x,y,z) ((x) >= (y) && (x) <= (z))\n#define MSGIDLIMIT_COMP(x,name) \\\n MSGIDLIMIT_BOUND((x),MSGIDLIMIT_##name##_MIN,MSGIDLIMIT_##name##_MAX)\n#define MSGIDLIMIT_TRANS(x) MSGIDLIMIT_COMP((x),TRANS)\n#define MSGIDLIMIT_TRANS_GEN(x) MSGIDLIMIT_COMP((x),TRANS_GEN)\n#define MSGIDLIMIT_TRANS_ALGO(x) MSGIDLIMIT_COMP((x),TRANS_ALGO)\n#define MSGIDLIMIT_TRANS_KEX(x) MSGIDLIMIT_COMP((x),TRANS_KEX)\n#define MSGIDLIMIT_AUTH(x) MSGIDLIMIT_COMP((x),AUTH)\n#define MSGIDLIMIT_AUTH_GEN(x) MSGIDLIMIT_COMP((x),AUTH_GEN)\n#define MSGIDLIMIT_AUTH_METH(x) MSGIDLIMIT_COMP((x),AUTH_METH)\n#define MSGIDLIMIT_CONN(x) MSGIDLIMIT_COMP((x),CONN)\n#define MSGIDLIMIT_CONN_GEN(x) MSGIDLIMIT_COMP((x),CONN_GEN)\n#define MSGIDLIMIT_CONN_CHAN(x) MSGIDLIMIT_COMP((x),CONN_CHAN)\n#define MSGIDLIMIT_RESERVED(x) MSGIDLIMIT_COMP((x),RESERVED)\n#define MSGIDLIMIT_EXTENDED(x) MSGIDLIMIT_COMP((x),EXTENDED)\n#define MSGIDLIMIT_POST_USERAUTH(x) ((x) >= MSGIDLIMIT_CONN_MIN)\n\n\n#define CHANNEL_EXTENDED_DATA_STDERR WOLFSSH_EXT_DATA_STDERR\n\n/* Used when checking IsMessageAllowed() to determine if creating and sending\n * the message or receiving the message is allowed */\n#define WS_MSG_SEND 1\n#define WS_MSG_RECV 2\n\n/* dynamic memory types */\nenum WS_DynamicTypes {\n DYNTYPE_STRING = 500,\n DYNTYPE_CTX,\n DYNTYPE_SSH,\n DYNTYPE_CHANNEL,\n DYNTYPE_BUFFER,\n DYNTYPE_ID,\n DYNTYPE_HS,\n DYNTYPE_CA,\n DYNTYPE_CERT,\n" + }, + { + "old": "\n\n#define WOLFSSL_V5_0_0 0x05000000\n#define WOLFSSL_V5_7_0 0x05007000\n#define WOLFSSL_V5_7_2 0x05007002\n\n\n#ifdef __cplusplus\n}\n#endif\n\n#endif /* _WOLFSSH_INTERNAL_H_ */\n\n", + "new": "\n\n#define WOLFSSL_V5_0_0 0x05000000\n#define WOLFSSL_V5_7_0 0x05007000\n#define WOLFSSL_V5_7_2 0x05007002\n\n\n#ifdef __cplusplus\n}\n#endif\n\n#endif /* _WOLFSSH_INTERNAL_H_ */\n" + } + ] + } +} diff --git a/tools/wolfssh_order/provenance.json b/tools/wolfssh_order/provenance.json new file mode 100644 index 0000000..03e474c --- /dev/null +++ b/tools/wolfssh_order/provenance.json @@ -0,0 +1,51 @@ +{ + "793": { + "url": "https://patch-diff.githubusercontent.com/raw/wolfSSL/wolfssh/pull/793.patch", + "sha256": "66aa999521800371f97b340db2bff162be4e8ffab4b4b953f9d32b8e33f56cea", + "commits": [ + "5fa6c0fce30a421879b355a007ef843eb48332d3", + "af45bc3719ddeac112d9d70b2e6a969f1aa3f3e7", + "d74c942c84d44fb46d3a10cb56233b704733e466", + "ff95f3c3029d766b114a91d98b013e0a1636a6c1", + "2a11471bb717a2ee6f06b3e1beab8a3e2b0ef261", + "813ec263cc56e7c9093135d854b3fc887633d368", + "cc17941a6125daefbadb8c236fdeaeb1a21ec786", + "4862400a374253216e596ff5c3b018b857015cbb" + ] + }, + "819": { + "url": "https://patch-diff.githubusercontent.com/raw/wolfSSL/wolfssh/pull/819.patch", + "sha256": "8e63c2b24679a7d831f7dba12e412d2f8a7fdc391fc4e8fd33f029487110219b", + "commits": [ + "201029797b260eee894b12d488bce6022290bf67" + ] + }, + "840": { + "url": "https://patch-diff.githubusercontent.com/raw/wolfSSL/wolfssh/pull/840.patch", + "sha256": "23e25873cb4dfa36063357111019e0960dfc008a68cc29d6e2d5c37bde47ec94", + "commits": [ + "9dc1071da7e560db2ea899fa23aab885a25ea862", + "024b14124aa2434e90468408b94121c897311f37" + ] + }, + "855": { + "url": "https://patch-diff.githubusercontent.com/raw/wolfSSL/wolfssh/pull/855.patch", + "sha256": "b94393df9528f66f6df1aba94015331fee3110c9ea2007a0eff369d8887b4b29", + "commits": [ + "73b165651c80a20047d913d32b0ffac47cb99ef6", + "a87ab400b3900d1e7fdda33c898094d6e3ada21d", + "5ae5c250e2d9b3bfdaea4f9c73cdf4b9f2592daa", + "dee1c59f263220ecd05a88a72da104c22bef0598", + "2e5484f36f279484ca06e02ef34ddd420fc0cf87", + "03ca9221a36a2a7160a99aeee7bc7ea8368013b5", + "2086f34ff37911a1fa388f843ee9d336e740bf44" + ] + }, + "921": { + "url": "https://patch-diff.githubusercontent.com/raw/wolfSSL/wolfssh/pull/921.patch", + "sha256": "f6c88598d7f3c94d92c31cecc4ef1e6d779b49f9eff6556ac1c57426d272d504", + "commits": [ + "e9d288ec603531a1d544e77fb1bbdf634cb2a57f" + ] + } +}