Harden SSH parsing and add notice tooling

- Enforce exact service and channel names with bounded failure parsing
- Add hash-pinned offline notice assembly and regression coverage
- Record advisory dispositions, provenance, integration evidence, and
  remaining gates
This commit is contained in:
2026-09-16 15:06:38 +02:00
parent bea33e1c95
commit 51f835c22f
29 changed files with 3332 additions and 46 deletions
+8 -3
View File
@@ -14,7 +14,9 @@ ESP32-S3 firmware for a secure, multi-transport RS-232 adapter. It operates one
## Development status
Hardware characterization, serial/USB/Wi-Fi/HTTPS/SSH and local display/control are implemented and hardware-validated. **Phase 8 role-based users and administration is complete:** 8A8C were target-hardware validated and the user explicitly signed off tested firmware at **8D.22 (2026-09-13)**. See the [roadmap](docs/roadmap.md#phase-8--role-based-users-and-administrative-access--complete) and [acceptance evidence](docs/web_administration_acceptance.md). Very low internal/DMA lifetime minima remain a nonblocking headroom follow-up, not an approved reserve. **Phase 9 security hardening is in progress**: 9A crash/debug policy, 9B SSH admission/credential handling and 9C library cleanup/protocol policy have passed host/build checks. **9D maintenance and lifecycle is in progress:** an operator runbook, bounded license inventory and partial wolfSSL/wolfSSH advisory review are available. Current-path advisory findings and distribution/corresponding-source/notice questions remain unresolved; **Phase 9 is not complete or production-ready**. The user will validate Phase 9 as a whole; no 9D device validation or license clearance is claimed. See [security hardening](docs/security_hardening.md) for scope, operational profiles, and validation gates.
Hardware characterization, serial/USB/Wi-Fi/HTTPS/SSH and local display/control are implemented and hardware-validated. **Phase 8 role-based users and administration is complete:** 8A8C were target-hardware validated and the user explicitly signed off tested firmware at **8D.22 (2026-09-13)**. See the [roadmap](docs/roadmap.md#phase-8--role-based-users-and-administrative-access--complete) and [acceptance evidence](docs/web_administration_acceptance.md). Very low internal/DMA lifetime minima remain a nonblocking headroom follow-up, not an approved reserve. **Phase 9 security hardening is in progress**: 9A crash/debug policy, 9B SSH admission/credential handling and 9C library cleanup/protocol policy have passed host/build checks. **9D maintenance and lifecycle is in progress:** an operator runbook, finite SSH parser/caller and IDF applicability reviews, and offline notice assembly tooling are available. Scoped corrections have parent host/build evidence; broader advisory coverage, release delivery/source/legal questions and whole-phase target gates remain open; **Phase 9 is not complete or production-ready**. The user will validate Phase 9 as a whole; no 9D device validation or license clearance is claimed. See [security hardening](docs/security_hardening.md) for scope, operational profiles, and validation gates.
Latest supplied parent integration: `pio run` **PASS**, **94,340 B linked RAM / 1,768,901 B flash** (+200 B flash); **seven focused suites PASS**. See [exact counts and evidence limits](docs/security_hardening.md#ordering-hostbuild-evidence--2026-09-16). Linked size and host passes are not target headroom or release acceptance.
### Browser administration
@@ -35,7 +37,10 @@ Keep UART0 ready for administrative recovery and native USB for network-independ
- [Security hardening](docs/security_hardening.md): Phase 9 scope, crash/debug baseline, diagnostic handling, and pending validation.
- [Security operations](docs/security_operations.md): provisioning, explicit saves, rotation, recovery, reconstruction and approved destructive reset/decommissioning; procedures, not device evidence.
- [Dependency license inventory](docs/dependency_licenses.md): bounded local inventory and unresolved distribution/source/notice obligations, not release clearance.
- [wolfSSL/wolfSSH advisory review](docs/wolf_security_review.md): partial upstream review, current-path priorities and unverified mitigation candidates.
- [wolfSSL/wolfSSH advisory review](docs/wolf_security_review.md): implemented scoped mitigations, historical research and remaining target/maintenance gates.
- [Remaining SSH parser review](docs/ssh_parser_remaining_review.md): completed finite server-profile dispatch/caller review, corrections and explicit exclusions; not exhaustive parser safety.
- [IDF applicability review](docs/idf_security_review.md): six named findings and a bounded advisory-index feature screen, not all-CVE clearance.
- [Release notice packaging](docs/release_packaging.md): offline [notice tool](tools/release_notices.py), deterministic bounded assembly and separate recipient/source/legal gates.
- [Command reference](docs/command_reference.md): UART0/admin-SSH administration, serial, broker, USB, Wi-Fi, mDNS, web, SSH, and diagnostic commands.
## Flash partition layout
@@ -104,7 +109,7 @@ The Phase 9A supported build baseline requires disabled core dumps and silent pa
## License
This project is licensed under the [GNU General Public License version 3 only](LICENSE) (`GPL-3.0-only`). Third-party components remain subject to their respective licenses. The integration baseline uses Espressif registry components `espressif/mdns` `1.12.0`, `wolfssl/wolfssl` `5.8.2~1`, and `wolfssl/wolfssh` `1.4.20`; see the [partial advisory review](docs/wolf_security_review.md) for unresolved findings, not merely a recommendation to check later. The [license inventory](docs/dependency_licenses.md) records open radio-blob corresponding-source/exception questions, notice delivery and preferred-source requirements, generated-file modification notices, and wolfSSH packaging clarification. Neither this license declaration nor the inventory clears firmware/device distribution.
This project is licensed under the [GNU General Public License version 3 only](LICENSE) (`GPL-3.0-only`). Third-party components remain subject to their respective licenses. The integration baseline uses Espressif registry components `espressif/mdns` `1.12.0`, `wolfssl/wolfssl` `5.8.2~1`, and `wolfssl/wolfssh` `1.4.20`; see the [partial advisory review](docs/wolf_security_review.md) for scoped corrections and remaining review/target gates, not a whole-library safety claim. The [license inventory](docs/dependency_licenses.md) records open radio-blob corresponding-source/exception questions, notice delivery and preferred-source requirements, generated-file modification notices, and wolfSSH packaging clarification. Neither this license declaration nor the inventory clears firmware/device distribution.
### Legacy credential removal
+8 -3
View File
@@ -21,13 +21,18 @@ This is a semantic map, not a complete file inventory. Start here, then read the
- 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 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.
- 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 explicit dormant-profile exclusions; [IDF review](../idf_security_review.md) records three backports, finite six-finding applicability completion and first-page feature exclusions, not exhaustive advisory safety.
- Parser tests: `tests/wolfssh_parser_contract/run.py`, [scope and exclusions](../../tests/wolfssh_parser_contract/README.md): 3,258 base cases per each of two stack modes plus five channel profiles in both modes (2,737/profile/mode for TERM, TERM+SHELL, TERM+SHELL+AGENT; 2,735 for neither/SHELL-only); 11 base + 18 name/length + 2 application-gate mutations rejected. Guard pages/UBSan and crypto doubles test parsing/gating, not arithmetic. [Finite review](../ssh_parser_remaining_review.md) closes exact CHANNEL_FAILURE recipient, ssh-userauth service and nine channel-name dispatch checks; PR899 client skips remain unchanged behind role/ordering gates, forwarding disabled, generic caller trace dispositioned but APIs unpatched. Unknown/trailing behavior is retained; not exhaustive safety. Ordering has its separate suite below.
- Latest supplied parent `pio run` PASS: **94,340 B linked RAM / 1,768,901 B flash (+200 B versus 1,768,701 B)**. Final parent **all seven 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, notices (30), and parser (3,258 × two modes plus channel profiles; 11 + 18 + 2 rejected mutations). 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.
## Offline release notice assembly
- [Procedure and release gates](../release_packaging.md), [tool](../../tools/release_notices.py), catalog `third_party/release-notices/inputs.json`, scope/TLSF provenance under `third_party/release-notices/`; tests: `tests/release_notices/run.py` (parent 30 PASS). 62 explicit hash/size-pinned inputs; no recursive collection, fetch, build, asset regeneration or device access; output must be fresh and outside input roots.
- Supplied independent review: no actionable scoped parser/bundle defects; two actual 62-input bundles deterministic. Historical actual bundle: 64 files / 541,147 bytes, not remeasured here. The catalog does not pin these overview/review documents. Assembly is implemented; recipient delivery, corresponding source, legal/radio-blob basis, icon provenance and whole-phase target gates remain open. Do not equate a notice manifest with a source archive, complete SBOM or release clearance.
## Secure randomness
**Responsibility:** provide the sole project-owned, mutex-serialized application DRBG, seeded before Wi-Fi/radio use.
+8
View File
@@ -2,6 +2,14 @@
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 — deferred applicability / notice tooling — 2026-09-16
- Initial Git clean. `tools/security_overrides.py` now requires exact ssh-userauth service, exact bounded CHANNEL_FAILURE recipient with existing fatal policy, and length-first exact names for all nine channel requests. Supported bodies and unknown-request ACK/consumption retained. No ordering/crypto/version changes. `docs/ssh_parser_remaining_review.md` closes finite PR899/902/918/919 and generic current signature caller trace: client key-skip functions remain unpatched but blocked by current server ordering; forwarding absent; no current short-digest/OID trigger found. Not exhaustive parser certification.
- `docs/idf_security_review.md` adds six named advisory dispositions with original/generated/config evidence: ECDH callers sized safely, driver-only PK path excluded, no untrusted CA promotion, privileged-local ECC sidechannel excluded not fixed, server tickets off despite compiled support, fixed CN avoids ASN zero-length replacement. No additional current-path source fix established; generic library defects remain explicitly recorded.
- `tools/release_notices.py` + `third_party/release-notices/inputs.json` assemble62 exact hash-pinned allowlisted notice inputs offline into fresh explicit output, descriptor no-follow traversal, bounded preflight, exclusive creates/no overwrite, deterministic names/bytes. No secret scanning/full repo/config/build collection. Tests30PASS; agent+independent reviewer verified two actual deterministic bundles and hashes. Historical64files541147bytes not general future promise. `docs/release_packaging.md` separates notices from source/recipient/legal obligations. No distribution clearance: radio source exception question, wolf package discrepancy, exact icon provenance and recipient delivery remain open.
- Parent `pio run` PASS **94,340 B linked RAM / 1,768,901 B flash** (+200flash). Parent seven suites PASS: parser3,258x2 plus channel production2,737x2 and fouralternate profiles, 11parser+18name+2appgate mutations; release notices30; auth135; protocol; strict crypto; SDK actual8C+header registration; ordering8,028/7mutations and12OpenSSH sessions each256KiB exact with rekey/cleancloseEOF. Independent review no actionable scoped defects, reran parser/notices and actual bundle determinism. Host evidence not hardware/resource/signoff.
- README/roadmap/hardening/wolf/license/code map integrated latest evidence and finite closure. **Next:** use hardeningdoc remaining implementation/maintenance, release/source/delivery and whole-phase target gates rather than repeatedly reopening completed finite reviews. Broader advisory coverage/unpatched unused primitives not certified; release packaging does not solve legal/source/provenance questions. User whole-phase validation remains deferred; secureboot/encryptedNVS excluded. No device/asset regeneration/dependency upgrade/commit.
## 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.
+6 -4
View File
@@ -4,7 +4,7 @@
**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 `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.
Offline notice assembly tooling is implemented; actual firmware/device/browser delivery, radio-blob corresponding-source/exception questions, preferred-source packaging, wolfSSH license-package discrepancy, exact icon provenance and release-specific runtime/bootloader review remain open. [Offline notice assembly](release_packaging.md) is implemented by [tools/release_notices.py](../tools/release_notices.py): 62 mandatory hash/size-pinned inputs, deterministic bounded outputs, fail-closed preflight and no overwrite/fetch/build/device access. Parent notices suite: 30 PASS. Supplied independent review found no actionable scoped parser/bundle defects and verified two actual 62-input bundles were deterministic. The previously measured actual bundle was 64 files / 541,147 bytes; that is snapshot evidence, not a newly measured bundle size or legal clearance. 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
@@ -61,7 +61,7 @@ Provenance references (not fetched): installed manifests point to [esp-usb](http
| HTTP parser **2.7.0** | `SDK/components/http_parser/LICENSE.txt`, `http_parser.h` in that component | `MIT`; selected. Preserve NGINX/Igor Sysoev and Joyent/Node attribution in the actual text. |
| argtable3, IDF snapshot | `SDK/components/console/argtable3/LICENSE` | **Aggregate**, including `BSD-3-Clause`, `BSD-2-Clause`, and `TCL` terms. It includes Stewart Heitmann, NetBSD getopt, Tcl, C Hash Table, and Better String notices. Do not reduce the entire file to one BSD label. `libconsole.a` selected; sublibrary/object attribution not exhaustively traced. |
| linenoise, IDF snapshot | `SDK/components/console/linenoise/LICENSE` | `BSD-2-Clause`, Salvatore Sanfilippo and Pieter Noordhuis; console selected. |
| TLSF allocator, IDF snapshot | `SDK/components/heap/tlsf/include/tlsf.h`, `SDK/components/heap/tlsf/tlsf_block_functions.h` | `BSD-3-Clause` SPDX headers, Matthew Conte 20062016; heap selected. No standalone TLSF license text appeared in the license-file inventory; obtain/assemble the correct full notice for release, not a generic BSD text with another author's name. |
| TLSF allocator, IDF snapshot | `SDK/components/heap/tlsf/include/tlsf.h`, `SDK/components/heap/tlsf/tlsf_block_functions.h` | `BSD-3-Clause` SPDX headers, Matthew Conte 20062016; heap selected. No standalone TLSF license text appeared in the historical inventory; offline assembly now retains a scoped full TLSF rendering and provenance. Final object coverage/delivery still require review, not a generic BSD text with another author's name. |
| Xtensa HAL, IDF snapshot | `SDK/components/xtensa/include/xtensa/hal.h` opening notice | `MIT`-style full permission notice, Cadence 19992015; `libxt_hal.a`/`libxtensa.a` selected. Header evidence is not an audit of every HAL object or ROM implementation. |
| Espressif prebuilt radio libraries, IDF snapshot | `SDK/components/esp_wifi/lib/LICENSE`, `SDK/components/esp_phy/lib/LICENSE`, `SDK/components/esp_coex/lib/LICENSE` | All three files are byte-identical to `SDK/LICENSE` (`Apache-2.0`). Map selects Wi-Fi `libcore.a`, `libespnow.a`, `libmesh.a`, `libnet80211.a`, `libpp.a`, and PHY `libbtbb.a`, `libphy.a`. `libesp_coex.a` is selected but that does not prove `libcoexist.a` was selected. **Corresponding-source/system-library question remains open**, despite permissive binary redistribution terms. |
| Newlib **4.3.0** and IDF libc integration | `TC/xtensa-esp-elf/include/_newlib_version.h`; `TC/share/licenses/newlib/COPYING.NEWLIB`; `SDK/components/newlib/COPYING.NEWLIB` | Mixed per-file licenses; **no single SPDX expression established**. Toolchain notice file has 1,293 lines vs SDK copy's 952; preserve the actual runtime package's notices, not just the older SDK summary. `libc.a` and `libnewlib.a` selected. Target-specific LGPL/GPL sections for other architectures/Linux are not proof they apply to ESP32-S3; trace selected objects before making that claim. |
@@ -134,11 +134,13 @@ For Apache-2.0 components, retain required attribution and any applicable upstre
## Actionable release work (not performed)
Notice assembly is implemented; the remaining release actions below have not been completed.
| Priority | Action / completion evidence |
| --- | --- |
| 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. |
| Assembly tooling implemented; delivery pending | [Offline tool and procedure](release_packaging.md) assemble 62 pinned inputs, retaining GPL/Apache/MIT/BSD texts, mixed SDK/toolchain Newlib and argtable3 notices, GCC exception and a scoped full TLSF rendering. Parent fixture suite: 30 PASS; two actual bundles independently verified deterministic. Validate final firmware/device/browser recipient delivery; assembly is not corresponding-source delivery or legal clearance. |
| Before distributing icon derivatives | Full Apache-2.0 text is supplied in the scoped bundle via SDK LICENSE; still 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 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. |
+82
View File
@@ -1,5 +1,87 @@
# Focused installed ESP-IDF security applicability review
## Finite applicability completion — 2026-09-16
**Current disposition supersedes the historical recommendations below.** This slice covers exactly the six previously named mbedTLS findings and the disabled/unused-feature entries on the first page of the official IDF advisory index. It is **not an exhaustive CVE inventory, security certification, upgrade approval, or Phase 9 sign-off**. Only this document was edited; no source/configuration/generated-file edits, build, test compilation, or device commands were performed.
### Effective source and configuration evidence
Rechecked installed **ESP-IDF 5.5.0 / mbedTLS 3.6.3**, not just upstream release ranges. Evidence is the existing `.pio/build/esp32-s3-devkitc-1-n16r8/` configuration and compilation database, installed source under `/home/mscholz/.platformio/packages/framework-espidf/`, and the checked-in override renderer. This establishes configured inputs, **not executable freshness or the firmware running on a device**.
- The current registry has **eight C replacements plus one wolfSSH internal-header overlay**. The seven-source count in the September 15 addendum is historical. Read-only `python3 -B` inspection called `verify_version()` and `render_entry()` only for the **six IDF-owned entries**, checked original hashes, and compared rendered bytes against existing generated copies: all six matched. No generator or build was run; wolfSSH bytes were outside this slice.
- Filtered compilation entries select generated `dhcpserver.c`, `ssl_tls.c` and `x509_create.c`, but original `ecdh.c`, `pk_ecc.c`, `pkparse.c`, `ecp_curves.c`, `asn1write.c`, `x509_crt.c`, `ssl_tls12_server.c` and `httpd_ws.c`. None of the six newly classified findings has a project source backport. The mbedTLS entries use `MBEDTLS_CONFIG_FILE="mbedtls/esp_config.h"`; inspected command flags add no alternative user config. `esp_config.h` includes the generated `sdkconfig.h` and maps its ECP/NIST options to the library macros.
- Generated header: `CONFIG_MBEDTLS_ECP_C=1`, `CONFIG_MBEDTLS_ECP_NIST_OPTIM=1`, `CONFIG_MBEDTLS_ECDSA_DETERMINISTIC=1`, TLS 1.2 and client/server support enabled, TLS 1.3 absent. P-521 is enabled (maximum ECP output 66 bytes). Hardware MPI support is enabled, **not** a replacement for all software ECC reduction. Both mbedTLS client/server ticket options are enabled, but **`CONFIG_ESP_TLS_SERVER_SESSION_TICKETS` is absent**. Do not describe tickets as globally compiled out.
- Rechecked generated EMS error branch (`mbedtls_ssl_tls/ssl_tls.c:70837086`) returns before derivation, and generated X.509 OID allocation (`mbedtls_x509_create/x509_create.c:313317`) checks NULL before copying. These fixes are separate from ECDH, PK parsing, basicConstraints and ASN.1 value-state issues.
Additional installed-source SHA-256 evidence (paths below relative to `components/mbedtls/mbedtls/library/`; earlier original hashes remain unchanged):
| Source | SHA-256 |
| --- | --- |
| `ecdh.c` | `85439414902f8421db8921c69a0be771ed69a7c69f0722127298f41a1d330ba8` |
| `pk_ecc.c` | `19ba6ca1251a610a53e3654a7727ba881c358a29d46f4586ab3c7e07c846b5bf` |
| `pkparse.c` | `36c0590c516a1b4ad99bfefc213da1a19a2e27638ee3f267ea3365cc738ecf17` |
| `x509_crt.c` | `799f6993d323d9158bf832967f404e9c63b0d76c0a72023be7a80c42630020fd` |
| `ecp_curves.c` | `29329ed2ecad6e68f615b1b5a3e7c592a0b625c4d1a1090280e305fa9afb51d3` |
| `ssl_tls12_server.c` | `ddf8d37e862d4176d27b928592b1971d2f397f50c4b1fac2443d099c3e9cab99` |
### Disposition matrix
**Addressed** means the specified source correction exists, not target acceptance. **Not-current-path** means this finding's prerequisite is absent in the inspected configuration/callers, not that the library is patched. **Excluded** is a threat-model boundary, not technical immunity. **Unresolved** marks evidence/coverage still absent.
| Finding | Disposition | Current evidence and boundary |
| --- | --- | --- |
| CVE-2026-45160 DHCP; CVE-2026-50581 TLS 1.2 EMS; CVE-2026-34874 X.509 OID OOM | **Addressed, scoped source corrections** | Existing pinned generated inputs match the renderer. Earlier host/build evidence remains attributed below; not rerun. EMS correction is TLS 1.2 only. |
| CVE-2026-35336 ECDH small output buffer | **Not-current-path; primitive unpatched** | `ecdh.c:649659` compares `mpi_size(z)` but writes curve-width bytes. HTTPS TLS premaster storage includes `MBEDTLS_ECP_MAX_BYTES`; no application direct caller supplies a small buffer. See caller qualification below. |
| CVE-2026-50583 zero-length ECC PK parse | **Not-current-path / configuration not affected** | Advisory explicitly excludes built-in ECC in 3.x. `MBEDTLS_ECP_C` is enabled; `pk.h:179182` requires it to be **absent** for `MBEDTLS_PK_USE_PSA_EC_DATA`. Active ECP-backed parser rejects `ilen < 1` before inspecting the point. |
| CVE-2026-49300 basicConstraints | **Not-current-path; parser unpatched** | Both lax parsing behaviors exist. No untrusted certificate/trust-root enrollment or HTTPS client-certificate authentication was found; local identity validation requires CA=false. No current cross-validator CA promotion path established. |
| CVE-2026-54435 ECC optimized reduction side channel | **Excluded attacker model; affected code active** | NIST optimization and deterministic ECDSA are enabled; P-256 identities use mbedTLS. Privileged-local precise execution traces / physical side channels are outside this network-focused review. No constant-time, hardware immunity, or key-extraction-resistance claim. |
| CVE-2026-50586 NewSessionTicket disclosure | **Not-current-path; server tickets disabled, primitive unpatched** | HTTPS defaults `session_tickets=false`, application does not override it; ESP-TLS callback registration is additionally compiled out. mbedTLS ticket machinery itself remains enabled. |
| CVE-2025-48965 stale ASN.1 value length | **Not-current-path; primitive unpatched** | `asn1write.c:412415` still clears pointer without length. Certificate creation uses fresh subject/issuer lists, each a single fixed-format nonempty CN. Exact upstream one-line fix is now resolved below. |
| CVE-2026-45541 WS subprotocol | **Not-current-path; SDK unpatched** | Existing manual-admission/NULL-subprotocol disposition below remains; not reclassified as globally fixed by this slice. |
| Target fault behavior, local side-channel feasibility, running firmware, advisories beyond this finite set/index page | **Unresolved / not evaluated** | No runtime measurements, fault injection, exhaustive inventory or target acceptance supplied by this review. Revisit applicability whenever callers, features or trust enrollment change. |
### Evidence for the six named findings
**CVE-2026-35336:** fetched the official [ECDH advisory](https://raw.githubusercontent.com/Mbed-TLS/mbedtls-docs/main/security-advisories/mbedtls-security-advisory-2026-07-possible-buffer-overflow-ecdh-calc-secret.md) and [source fix `1d71bcc31cb8080b14b0dcbc6bc859c11d622c0f`](https://github.com/Mbed-TLS/mbedtls/commit/1d71bcc31cb8080b14b0dcbc6bc859c11d622c0f.patch). First fixed 3.6.x release: **3.6.7**. Installed vulnerable comparison matches the removed hunk. `ssl_tls12_server.c:37853789` passes `handshake->premaster`; `ssl_misc.h:990` sizes it as `MBEDTLS_PREMASTER_SIZE`, whose union in `ssl.h:691724` includes `_pms_ecdh[MBEDTLS_ECP_MAX_BYTES]`. Although the TLS call's `blen` argument is `MBEDTLS_MPI_MAX_SIZE`, the **actual object**, not merely that argument, is sufficient for every enabled ECDH curve. The advisory explicitly says TLS ECDH is unaffected. Targeted `src/` search found no direct `mbedtls_ecdh_calc_secret()` caller.
Also checked the installed Wi-Fi adapter rather than assuming HTTPS is the only SDK consumer: `wpa_supplicant/esp_supplicant/src/crypto/crypto_mbedtls-ec.c:803` uses the DPP maximum 66; DPP is not enabled in the generated configuration. Its `crypto_ecdh_set_peerkey()` at line 1290 uses `pbits / 8`; the inspected OWE caller in `src/rsn_supp/wpa.c` restricts both offered/received group to **19 (P-256)**, so this does not supply the advisory's non-byte-aligned P-521/floor-size trigger. OWE support **is** compiled in; this is not a blanket audit of its parsing or every SDK ECDH wrapper. Future arbitrary-curve callers must allocate and pass at least `(pbits + 7) / 8`, or use `MBEDTLS_ECP_MAX_BYTES`. A pinned `ecdh.c` backport is defense in depth, not a demonstrated current HTTPS overflow correction.
**CVE-2026-50583:** fetched the official [zero-length ECC public-key advisory](https://raw.githubusercontent.com/Mbed-TLS/mbedtls-docs/main/security-advisories/mbedtls-security-advisory-2026-07-zero-length-ecc-public-key-oob-read.md). It affects driver-only PSA ECC in 3.5.03.6.6, not built-in ECC. Original `pk_ecc.c:204220` has the unchecked `*pub` only under `MBEDTLS_PK_USE_PSA_EC_DATA`; its active `#else` calls `mbedtls_ecp_point_read_binary()`, whose `ecp.c:775777` checks `ilen < 1`. Application PK parsing **does occur**, in `src/web_security.c:409414` and `src/ssh_security.c:156181`, as well as ESP-TLS identity loading. Neither trusted storage nor a post-parse key check is being used as the exclusion: the decisive evidence is the built-in ECP path. Advisory identifies `0e2d7037db4048dbf1c194508c07384a818261d5` as the 3.6.x fix; no backport is installed or needed to close this configuration-specific finding. Reassess before moving to driver-only ECC.
**CVE-2026-49300:** fetched the official [basicConstraints advisory](https://raw.githubusercontent.com/Mbed-TLS/mbedtls-docs/main/security-advisories/mbedtls-security-advisory-2026-07-x509-ca-bit-forgery-invalid-basicconstraints.md). Installed `x509_crt.c:494555` lacks the inner-SEQUENCE length equality check and accepts INTEGER in place of BOOLEAN. The extension caller at line 966 supplies `end_ext_octet`, so comparison against the exact inner sequence is still necessary. First fixed 3.6.x release: **3.6.7**.
Current `src/web_security.c` generates its own P-256 self-signed identity, `basicConstraints(false, -1)`, and parses persisted identity at lines 409414. Validation checks matching keys, expected subject/issuer, self-signature, and specifically rejects `ca_istrue != 0` at lines 442444. `src/web_server.c:673691` retains default `cacert_pem=NULL`; generated ESP-TLS selects `MBEDTLS_SSL_VERIFY_NONE` for server peer authentication (`CONFIG_ESP_TLS_SERVER_MIN_AUTH_MODE_OPTIONAL` absent). Application authentication is not certificate-chain enrollment. Targeted application searches found no outbound ESP-TLS/HTTP client or untrusted certificate/CSR import; Wi-Fi configuration uses personal PSK/SAE profiles, not EAP credential/CA provisioning. These are current-use observations, **not global removal of X.509/client/enterprise support** (those libraries/features are available). A corrupt local blob being rejected is not evidence that the library parser is fixed.
Backport provenance correction: fetched [sequence fix `07f45b87681c1a0680c260089d3e6349b25fdd08`](https://github.com/Mbed-TLS/mbedtls/commit/07f45b87681c1a0680c260089d3e6349b25fdd08.patch). The advisory's listed “Basic fix #2,” [`4fb9c9e439fd1e7e44697d23d50f00b4642fbe08`](https://github.com/Mbed-TLS/mbedtls/commit/4fb9c9e439fd1e7e44697d23d50f00b4642fbe08.patch), changes **test encodings only**. A bounded official commit-history query for `library/x509_crt.c` at tag `mbedtls-3.6.7` located the actual [INTEGER-rejection source change `f67b8e5bded0c531b29480b8c5b7285366b1b153`](https://github.com/Mbed-TLS/mbedtls/commit/f67b8e5bded0c531b29480b8c5b7285366b1b153.patch), also fetched and compared. Do not backport the advisory's two listed hashes and claim both source defects fixed. No parser backport or runtime vector validation was performed here.
**CVE-2026-54435:** fetched the official [optimized modp side-channel advisory](https://raw.githubusercontent.com/Mbed-TLS/mbedtls-docs/main/security-advisories/mbedtls-security-advisory-2026-07-ecc-optimized-modp-side-channel.md). First fixed 3.6.x release: **3.6.7**. Installed `ecp_curves.c` retains specialized P-256 reduction and data-dependent carry/sign handling (`NEXT`, `LAST`, `mbedtls_ecp_fix_negative`, around lines 49005036); no override replaces it. Application key generation/loading and certificate signing genuinely use P-256; this is **not an unused-crypto finding**. Upstream requires privileged-local sufficiently precise traces (typically an untrusted OS attacking an enclave); physical side channels are also possible, not measured here. Disabling NIST optimization is an upstream workaround for NIST curves only, carries substantial performance cost, and does not cover Koblitz/Montgomery curves. No configuration change is recommended blindly on this resource-constrained target. Reopen for a local/physical attacker threat model or deliberate maintained-library update; the upstream multi-commit fix range was identified in the advisory but not patch-audited in this excluded slice.
**CVE-2026-50586:** fetched the official [TLS 1.2 ticket disclosure advisory](https://raw.githubusercontent.com/Mbed-TLS/mbedtls-docs/main/security-advisories/mbedtls-security-advisory-2026-07-tls12-newsessionticket-information-disclosure.md). First fixed 3.6.x release: **3.6.7**. Original `ssl_tls12_server.c:42424246` still declares uninitialized `tlen`/`lifetime`; failed ticket generation may disclose four stack bytes. `esp_https_server.h:183192` defaults client CA to NULL and `session_tickets=false`; the application preserves both. Generated HTTPS `create_secure_context()` initializes tickets only under `config->session_tickets`; generated ESP-TLS registers callbacks only under `CONFIG_ESP_TLS_SERVER_SESSION_TICKETS` and non-NULL `cfg->ticket_ctx`. Neither prerequisite holds. This is disabled **server-side TLS session tickets**, not application WebSocket/admin tickets, cookies, or compiled-out mbedTLS ticket support. Reassess and backport/init both outputs before enabling the feature; advisory lists `99ccd257e2d6c5fc53bc970e3e533a90c363f8e1` and `548ed19f707565db5fb4c2487edd7ae1bea50199` (not patch-verified here).
**CVE-2025-48965:** re-fetched the official [ASN.1 advisory](https://raw.githubusercontent.com/Mbed-TLS/mbedtls-docs/main/security-advisories/mbedtls-security-advisory-2025-06-6.md). First fixed release: **3.6.4**. Rechecked original stale-length branch and fixed-format CN generation (`src/web_security.c:92111,128240`); subject and issuer are separate fresh lists, not repeated OIDs within one list. No application direct named-data/string-name API consumer besides those fixed name setters was identified. This is not the separately addressed OID-allocation failure. A bounded official history query for `library/asn1write.c` at `mbedtls-3.6.4` resolved and fetched [fix `2df7ab7c0c3d5bb8a31481073c494521d10d4eba`](https://github.com/Mbed-TLS/mbedtls/commit/2df7ab7c0c3d5bb8a31481073c494521d10d4eba.patch): exactly `cur->val.len = 0;` after clearing `cur->val.p`, plus repeated-OID tests. Its context matches installed lines 412415. **The older unresolved-fix-commit statement below is now resolved; the source bug remains unpatched.** If backported defensively, select actual nested target `mbedcrypto`, retain source pin/notices and allocation-failure preservation, and test nonempty→empty→same-length replacement and serialization.
### Bounded IDF advisory-index snapshot: disabled/unused features
Fetched [official IDF index, page 1](https://github.com/espressif/esp-idf/security/advisories) on **2026-09-16**: ten entries spanning February 3September 3, 2026, newest listed A2DP/L2CAP. Fetched each of the eight remaining feature advisories below; the other two are DHCP and WS already covered above. **Pages 23, broader historical/vendor advisory coverage and future publications were not reviewed.** Later affected-release labels do not establish that installed 5.5.0 is fixed; dispositions here rely on feature/target/caller absence, not release-table inference.
| Official advisory / CVE | Required feature | Snapshot disposition and local evidence |
| --- | --- | --- |
| [GHSA-xcpr-5mqp-9qvv](https://github.com/espressif/esp-idf/security/advisories/GHSA-xcpr-5mqp-9qvv), CVE-2026-81508 | Bluedroid Classic A2DP sink; paired BR/EDR source | **Not-current-path**: BT disabled; no BT host compile inputs. |
| [GHSA-v335-fxwc-rc44](https://github.com/espressif/esp-idf/security/advisories/GHSA-v335-fxwc-rc44), CVE-2026-81507 | Bluedroid Classic L2CAP configuration | **Not-current-path**: same disabled BT evidence; no claim that authentication would mitigate this unauthenticated path. |
| [GHSA-3pp8-42fh-3j3c](https://github.com/espressif/esp-idf/security/advisories/GHSA-3pp8-42fh-3j3c), CVE-2026-46532 | Bluedroid AVRCP target, paired BR/EDR peer | **Not-current-path**: same disabled BT evidence. |
| [GHSA-9r76-858f-v6jh](https://github.com/espressif/esp-idf/security/advisories/GHSA-9r76-858f-v6jh), CVE-2026-45542 | NimBLE protocomm Security2 provisioning | **Not-current-path**: BT/NimBLE absent; no application protocomm/Wi-Fi provisioning API use found. SoftAP itself is not this provisioning service. |
| [GHSA-h7r3-gmg9-xjmg](https://github.com/espressif/esp-idf/security/advisories/GHSA-h7r3-gmg9-xjmg), CVE-2026-25507 | BLE provisioning teardown with `keep_ble_on=true` | **Not-current-path**: same disabled BLE/no provisioning service evidence, not a verified vendor UAF fix. |
| [GHSA-v6r2-f6p2-88cj](https://github.com/espressif/esp-idf/security/advisories/GHSA-v6r2-f6p2-88cj), CVE-2026-55687 | Hardware JPEG decoder DQT parsing | **Not-current-path**: ESP32-S3 configuration, no `esp_driver_jpeg` compilation input or application JPEG calls. No malformed-image testing. |
| [GHSA-w82j-7q63-7pqm](https://github.com/espressif/esp-idf/security/advisories/GHSA-w82j-7q63-7pqm), CVE-2026-45329; [GHSA-mmgp-73p4-92xp](https://github.com/espressif/esp-idf/security/advisories/GHSA-mmgp-73p4-92xp), CVE-2026-45328 | ESP-TEE REE/TEE secure-service wrappers | **Not-current-path**: target is Xtensa ESP32-S3, not the advisory's supported RISC-V TEE SoCs; no `esp_tee` compilation inputs or enabled TEE setting. This exclusion is distinct from choosing not to implement secure boot/encrypted NVS. |
Local snapshot evidence: generated `sdkconfig.h` selects `CONFIG_IDF_TARGET="esp32s3"` / `CONFIG_IDF_TARGET_ARCH_XTENSA=1`; no enabled `CONFIG_BT_ENABLED`, `CONFIG_BT_BLUEDROID_ENABLED`, `CONFIG_BT_NIMBLE_ENABLED` or ESP-TEE option. Filtered existing compilation database counts for `/bt/host/`, `/esp_tee/`, `/esp_driver_jpeg/`: **0 each**. `src/` searches for provisioning/protocomm/JPEG APIs found none. This is a feature screen, not inspection or certification of the disabled vendor implementations. Re-enabling any feature invalidates its disposition.
### Parent handoff and remaining limits
**No new current-path actionable source correction established among these six named findings.** Keep the existing DHCP/EMS/OID-allocation fixes. Optional defense-in-depth candidates are the exact ASN.1 one-line fix, ECDH curve-width bounds check and both basicConstraints source fixes; they must not be represented as already implemented. The basicConstraints test-only commit mismatch is actionable **backport guidance**, not evidence of a present authentication bypass. Preserve server ticket disablement and re-review before introducing certificate imports, driver-only ECC, arbitrary-curve ECDH callers or local isolation requirements.
Completed evidence work: official six-advisory fetches, focused upstream patch comparisons/history queries, original-source/config/caller inspection, read-only equality checks of six IDF generated overrides, and filtered configured-input inspection. Document-scoped `git diff --check` and the corrected document-presence/link-count check passed (the initial checker incorrectly expected twice the number of advisory URLs). Concurrent changes appeared in other agents' parser/override/release-notice files during the review; none were edited or reverted here, and source/configuration evidence above is the inspected snapshot, not certification of subsequent changes. No exploits, new host regression tests, build or device validation were run. The finite applicability questions above are dispositioned; full advisory coverage, runtime fault/timing behavior and whole-phase target acceptance remain unresolved. **Do not summarize this as “all CVEs safe.”**
## Implementation addendum — 2026-09-15
**Implemented, with host/build evidence; not target validation or a completed advisory review.** Source checked against `tools/security_overrides.py`, `cmake/security_overrides.cmake` and the existing generated copies:
+152
View File
@@ -0,0 +1,152 @@
# Offline release notice packaging
This procedure produces a **bounded notice/provenance bundle**, not a firmware
release, corresponding-source archive, complete SBOM, or legal clearance.
The **radio-blob corresponding-source/System Library question remains open**.
See [the license inventory](dependency_licenses.md) for the original review and
[the retained scope/provenance statement](../third_party/release-notices/README.md)
for license choices, mixed notices, wolfSSH packaging discrepancy, TLSF text
assembly, icon limits, and remaining release gates.
## Run against the installed reviewed snapshot
Requirements: Python 3.9+ on Linux/POSIX with `O_NOFOLLOW`, directory descriptors
and `dir_fd` operations; already installed matching managed components,
ESP-IDF and Xtensa toolchain. Standard library only. The tool never fetches,
installs, builds, runs PlatformIO, accesses a device, or regenerates assets.
Run from the repository root. Supply **an explicit, not-yet-existing output
directory outside all three input roots**, with an existing parent. Example
for the inventory's local installation (choose a fresh final directory name):
```sh
python3 tools/release_notices.py \
--sdk-root /home/mscholz/.platformio/packages/framework-espidf \
--toolchain-root /home/mscholz/.platformio/packages/toolchain-xtensa-esp-elf \
--output /tmp/esp32-release-notices-review-01
```
`--project-root` optionally selects a different checkout containing the exact
reviewed inputs; by default it is the script's repository. The trusted catalog
always comes from the script's repository, not from that alternate checkout.
Paths to SDK/toolchain packages are explicit; no installation is guessed.
There is no user catalog option, force/overwrite mode, automatic repinning,
recursive collection, archive command, or automatic release upload.
Every input in `third_party/release-notices/inputs.json` is mandatory and pinned
by **full-file SHA-256 and size**. Missing sources, altered bytes, bad excerpts,
empty files, directories, FIFOs, symlinks (even to identical files), parent
traversal, binary notice payloads and exhausted bounds fail closed. Symlinks in
input-root or output-parent ancestry are rejected too: pass real non-symlinked
paths, not a convenience symlink. All input preflight completes before creating
output. The limits are 128 inputs, 4 MiB per input/catalog and 32 MiB cumulative
source bytes. The current catalog has 62 inputs (about 2.1 MB read and 0.51 MB
copied before the generated manifest/introduction).
An existing output file, directory (even empty) or dangling symlink is never
overwritten. Parent directories are not automatically created. Output uses
private directory/file modes (0700/0600, subject to umask). On a write failure,
the tool deliberately leaves any partial directory for inspection and never
recursively deletes it. A successful exit plus a valid manifest and matching
payload hashes is required; presence of a partial manifest is not success.
Use another fresh path for a retry. Do not put output into a tree that an
untrusted process can concurrently rename or modify; descriptor/no-follow
checks prevent symlink traversal but are not isolation from the same OS user.
## Bundle layout and reproducibility
- `README.txt`: limitations and entry point for recipients.
- `inputs/project/…`: project GPL text, resolved lock, managed licenses and
manifests, wolfSSL/wolfSSH README evidence, original wolfSSH header excerpts,
xterm MIT texts and recorded provenance, icon summary, TLSF full rendering
and scope/provenance statement.
- `inputs/sdk/…`: complete scoped SDK license documents, mixed Newlib/argtable3
notices, radio license files, package/version identity and original source
notice excerpts. SDK `LICENSE` supplies full Apache-2.0 text; it does **not**
authenticate individual icons or cover all SDK files.
- `inputs/toolchain/…`: actual toolchain Newlib text, GPLv3, GCC Runtime Library
Exception 3.1 and package identity. No toolchain executable is copied.
- `manifest.json`: schema/snapshot, catalog SHA-256, logical root/path,
full-source size/hash, optional zero-based half-open byte range, purpose,
output path/size/hash, and the generated introduction's hash. Excerpts have
`.notice.txt` appended; null ranges retain whole original files unchanged.
Whole mixed documents are preserved, not converted into guessed SPDX labels.
The longer toolchain Newlib notice does not replace the distinct SDK text.
The wolfSSH package GPLv2-or-later text and GPLv3-or-later source headers remain
separate and unmodified; upstream packaging clarification is still needed.
Generated metadata contains no collection timestamps or absolute host paths;
original notices retain their own dates and text. For identical catalog and
inputs, file names and file contents are deterministic across
output locations, input-root locations and source mtimes. Directory/file
mtimes are **not** normalized, and no reproducible tar/zip archive is claimed.
The manifest hashes all payloads but not itself; retain its hash in your
separate release evidence if needed. The catalog is trusted reviewed policy,
not a cryptographic signature or proof of package-wide integrity/authorship.
Only explicit catalog paths are read. No application config, sdkconfig,
build tree, ELF/map/binary, key, credential backup, NVS/flash dump or whole
repository is collected. Hash-pinned source implementations are read only to
validate original header excerpts, not copied as implementations. The lock and
installed package/component manifests are dependency identity metadata, not
build configuration. A hash pin is not a general-purpose secret scanner:
review additions to this trusted list before accepting them.
## Validate and inspect
Temporary-fixture regression suite (does not need installed packages):
```sh
python3 tests/release_notices/run.py
```
The suite tests missing/changed sources (including changes outside a notice
excerpt), exact mixed-text retention, byte ranges, path traversal, source/root/
output symlinks, existing user data, nonregular files, bounds, output failures,
secret/config exclusion, determinism, and CLI behavior. No hardware tests are
implied.
For a real bundle, check the successful process exit, parse `manifest.json`,
recompute every payload hash/size, check no unlisted files are present, and
read the scope statement. Compare two independently created fresh outputs by
relative names and bytes (not mtimes). Inspect at least both Newlib files,
argtable3, wpa_supplicant COPYING/README, GCC exception, wolfSSH LICENSE/README
and header excerpts, TLSF text/provenance, both xterm notices and the icon
summary. These checks establish scoped assembly, **not release compliance**.
If a pinned input differs, stop and review the changed package/license/source.
There is deliberately no update-catalog command. Review versions, new/nested
NOTICEs, original source notice ranges and hashes, then deliberately update
catalog policy and test it. The runtime does not discover added notices or
packages outside its finite list; this limitation makes its reads bounded and
prevents opportunistic collection of user files.
## Separate work before distribution
1. Resolve or obtain qualified review of radio-blob corresponding-source or
exception basis for the actual linked firmware. Apache binary permissions
alone do not settle this question. No legal clearance is claimed.
2. Resolve the wolfSSH package/header discrepancy and verify exact two-icon
provenance/licensing (recorded 7.4.47), including any applicable NOTICE.
Supplying the full Apache text is not provenance verification. No icon
assets or generated data were changed by this packaging work.
3. Audit the final application/bootloader/runtime object set and aggregation
boundaries. Full SDK/toolchain redistribution needs broader nested/tool/test
notice coverage; this is not an all-files license scanner.
4. Prepare and validate a separate, deliberately reviewed corresponding-source
delivery. Preserve exact required sources and safe build/install inputs,
asset preferred sources and scripts, original hash-matching overlay inputs,
`tools/security_overrides.py`, `cmake/security_overrides.cmake`, root CMake
integration, `cmake/wolf_crypto_policy.{cmake,h}`, and the complete
`tools/wolfssh_order/` inputs/provenance/archives. Preserve upstream notices
and generated dated change notices on eight C sources and one PUBLIC forced
header. A stock vendor tarball or this notice bundle is not sufficient.
Do not automatically archive the working repository or real configurations.
5. Assess GPLv3 source delivery/offer and User Product Installation Information.
Protect device passwords, Wi-Fi credentials, private keys, verifiers,
tickets, credential backups and flash/NVS images throughout that process.
6. Arrange and verify actual notice delivery with firmware, devices and browser
assets. This command does not add a license endpoint, change browser assets,
or prove that a recipient of a JS response or standalone device receives
notices. Keep these gates open until the real delivery path is validated.
+7 -1
View File
@@ -216,7 +216,13 @@ 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. **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.
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; the [finite remaining SSH parser/caller review](ssh_parser_remaining_review.md) is complete, not exhaustive safety. Exact CHANNEL_FAILURE recipient, `ssh-userauth` service and all nine channel-name checks are implemented; unknown/trailing-payload behavior is retained. PR899 client skips remain unchanged behind server ordering, forwarding is disabled, and the inspected generic caller trace has no short-digest/OID trigger; generic APIs are not globally fixed. **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. The [finite IDF applicability completion](idf_security_review.md#finite-applicability-completion--2026-09-16) dispositions all six named findings: ECDH small-output, zero-length ECC PK parse, basicConstraints, server NewSessionTicket and stale ASN.1 length are not current paths/configurations for their documented reasons; optimized ECC reduction is active but its privileged-local/physical side-channel attacker model is excluded. No new current-path correction was established. Only the first advisory-index page was screened; unpatched primitives and broader coverage remain, not “all CVEs safe.”
**Latest integration evidence:** supplied parent build PASS: **94,340 B linked RAM / 1,768,901 B flash**, unchanged RAM / **+200 B flash** versus 1,768,701 B. Final parent **all seven 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, notices (30), and parser (3,258 × two modes plus channel profiles; 11 + 18 + 2 rejected mutations). 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).
**Release status:** 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 provenance questions remain open. [Offline notice assembly](release_packaging.md) is implemented by [tools/release_notices.py](../tools/release_notices.py): 62 mandatory hash/size-pinned inputs, deterministic bounded outputs, fail-closed preflight and no overwrite/fetch/build/device access. Parent notices suite: 30 PASS. Supplied independent review found no actionable scoped parser/bundle defects and verified two actual 62-input bundles were deterministic. The previously measured actual bundle was 64 files / 541,147 bytes; that is snapshot evidence, not a newly measured bundle size or legal clearance.
**Remaining maintenance/target gates:** 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.
+10 -4
View File
@@ -105,9 +105,9 @@ 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). 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 **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.
- [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. The [finite remaining SSH review](ssh_parser_remaining_review.md) is complete: bounded exact CHANNEL_FAILURE recipient parsing (fatal policy retained), exact `ssh-userauth` service validation, and exact length/byte dispatch for all nine channel-request names are implemented. Unknown-request/trailing-payload behavior is preserved. PR899 client key skips are unchanged and blocked by current server role/ordering; PR918/919 forwarding is disabled. The inspected generic signature caller trace found no attacker-selected short-digest/OID path; generic APIs remain unpatched. These are profile-specific dispositions, not exhaustive parser/library safety; revisit on caller, feature, KEX or source changes. 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. The [finite IDF applicability completion](idf_security_review.md#finite-applicability-completion--2026-09-16) dispositions all six named findings: ECDH small-output, zero-length ECC PK parse, basicConstraints, server NewSessionTicket and stale ASN.1 length are not current paths/configurations for their documented reasons; optimized ECC reduction is active but its privileged-local/physical side-channel attacker model is excluded. No new current-path correction was established. Only the first advisory-index page was screened; unpatched primitives and broader coverage remain, not “all CVEs safe.”
- [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 exact icon provenance and recipient license delivery remain open. Notice assembly does not prove recipient delivery. [Offline notice assembly](release_packaging.md) is implemented by [tools/release_notices.py](../tools/release_notices.py): 62 mandatory hash/size-pinned inputs, deterministic bounded outputs, fail-closed preflight and no overwrite/fetch/build/device access. Parent notices suite: 30 PASS. Supplied independent review found no actionable scoped parser/bundle defects and verified two actual 62-input bundles were deterministic. The previously measured actual bundle was 64 files / 541,147 bytes; that is snapshot evidence, not a newly measured bundle size or legal clearance.
- **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. 9A9C passes below are historical scoped evidence, not closure of these newly recorded findings.
@@ -131,12 +131,14 @@ Raw flash, RAM and dumps can contain Wi-Fi passwords, private keys, password ver
### 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 parent `pio run` **PASS**: **94,340 B linked RAM / 1,768,901 B flash**, unchanged RAM / **+200 B flash** versus 1,768,701 B. **Supplied final parent results: all seven 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, notices (30), and parser (3,258 cases in each of two stack modes plus channel profiles; 11 + 18 + 2 rejected mutations). 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.
The [source-authoritative parser report](ssh_parser_remaining_review.md#validation-and-remaining-handoff) and [test contract](../tests/wolfssh_parser_contract/README.md) split the channel matrix from the 3,258 base cases: **2,737 per stack mode** for TERM-only, TERM+SHELL and TERM+SHELL+AGENT; **2,735 per stack mode** for no-terminal and SHELL-only. All five profiles run both modes; alternate features are host fixtures, not firmware enablement. Mutations are **11 base + 18 name/length + 2 real application admission gates**. Parent ordering adds **12 OpenSSH sessions**, each exact **256 KiB**, rekey and clean client/server exit; SDK tests checked actual build registration. These integrated parent results supersede the parser report's earlier stale-build handoff, without changing its historical execution record. Independent review reported no actionable defects within the scoped parser/bundle review, not a Phase 9 approval.
### 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.
@@ -206,6 +208,10 @@ Retain these checks for the user's final whole-phase test session; do not stop i
## Staged next work
- **Implementation/maintenance gate:** the finite SSH and six-finding IDF reviews are complete for their stated profiles; do not re-list them as unimplemented. Finish broader advisory coverage and resolve any newly established current-path findings with pinned changes and fresh production-source tests. Optional ASN.1/ECDH/basicConstraints defense-in-depth backports are not implemented or required by a demonstrated current-path finding.
- **Release gate:** notice assembly is implemented; validate actual firmware/device/browser delivery, corresponding source and preferred asset sources, radio-blob legal basis, wolfSSH packaging clarification, exact icon provenance, final runtime/bootloader attribution and any Installation Information. See [packaging gates](release_packaging.md#separate-work-before-distribution).
- **Target/acceptance gate:** rehearse operations and the combined target checklist above, including panic/recovery, abuse/isolation, cleanup during rekey, intended-client compatibility without EXT_INFO, loaded KEX/rekey and heap/stack/CPU timing. Obtain explicit whole-phase acceptance; no scoped reviewer or host PASS can substitute.
- **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.
+276
View File
@@ -0,0 +1,276 @@
# Remaining SSH parser review — 2026-09-16
## Completed scope and decision
**This finite review is complete for the current pinned server profile:** PR899
key skips and CHANNEL_FAILURE, PR902 service validation, PR918/919 forwarding
callback applicability, the remaining generic signature-API caller question, and
**exact `DoChannelRequest` name dispatch including application callback gates**.
Three bounded current-path corrections are implemented. This is not a complete
wolfSSH/wolfSSL audit, a full backport of these PRs, firmware validation, or Phase9
sign-off. It supplements the historical/deferred statements in
[wolf review](wolf_security_review.md), [key-validation review](ssh_key_validation_review.md)
and the [parser contract](../tests/wolfssh_parser_contract/README.md).
Only `tools/security_overrides.py`, `tests/wolfssh_parser_contract/*`, and this report
are owned by this slice. No ordering delta, crypto configuration, version, managed
component, application, production generated file, PlatformIO or device change.
Concurrent packaging/IDF-review work is unrelated and left untouched.
| Reviewed item | Current-profile disposition |
| --- | --- |
| PR899 RSA/ECC unchecked key skips | Confirmed in the pinned client parsers, not reachable through current server dispatch. No speculative client patch. |
| PR899 CHANNEL_FAILURE length predicate | Reachable after authentication. Corrected with a bounded exact recipient parser, not just the upstream predicate change. Existing fatal failure policy retained. |
| PR902 | **Service names**, not channel callbacks. Exact `ssh-userauth` required by current server handler. Client accept half unused/unmodified. |
| PR918/919 | Forwarding global/channel callback handling; compiled out with `WOLFSSH_FWD` absent. No forwarding patch. |
| `DoChannelRequest` prefix/NUL name aliases | Closed: all nine name predicates require exact length then exact bytes; branch bodies and unknown-request handling preserved. |
| Generic signature API / PR10131 remaining question | Weak generic API remains, but no attacker-selected short digest/OID path in the inspected current SSH caller set. No crypto/API patch justified for this profile. |
## Exact source and upstream provenance
The authoritative source is wolfSSH **1.4.20 original + existing ordering delta +
existing parser/password edits + the three corrections below**, not installed source
alone. `render_entry` verifies original SHA-256 and exact-once edit anchors. Tests
render into temporary files; production generated inputs are not overwritten.
| Input | SHA-256 |
| --- | --- |
| Original `managed_components/wolfssl__wolfssh/src/internal.c` | `81ff1f9166708abd5c2911e9fe57c0aee01c88b5d3f68c909ee8a856d37f36a9` |
| Reviewed prior original+ordering+parser generated `internal.c` | `4948f8c447670eb54153dd1f3db69e4fa3092f7d7f7ed58a18a8fa05fcd168ca` |
| Fresh generated `internal.c` after this review | `1fdd608d45c4f33da82b0765dc66e1ec2060e78c744bc539906ef1b8a0f783ae` |
| Unchanged `tools/wolfssh_order/delta.json` | `6a81376fe3ffc5f449cde105402963f52d2d78cc844153e869a7e1e0f734fb76` |
| wolfSSL 5.8.2 `wolfcrypt/src/signature.c` | `62ab3db3dfd251b2a2c73b69ef05aab6085d2e0d673fd9159514b3ee261cea4f` |
Fetched official PR patches and independently fetched their commit patches on
2026-09-16; each pair was **byte-identical**. Exact archives and URL/SHA-256 records
are in `tests/wolfssh_parser_contract/pr*.patch` and `provenance.json`. The runner
also pins those commits/hashes/URLs independently; no download occurs during tests
or configuration.
- [PR899 commit d2eeec5e263a4821c90805963eeb0666e99868a6](https://github.com/wolfSSL/wolfssh/commit/d2eeec5e263a4821c90805963eeb0666e99868a6): RSA algorithm skip, ECC curve skip, CHANNEL_FAILURE predicate; Windows file/terminal hunks unused on ESP.
- [PR902 commit ffa646a4b9d47d5d9d6127db140c433c58b1e276](https://github.com/wolfSSL/wolfssh/commit/ffa646a4b9d47d5d9d6127db140c433c58b1e276): service request/accept name checks.
- [PR918 commit fd82a4bcf55935f0801b14bca6be9c71e32ae914](https://github.com/wolfSSL/wolfssh/commit/fd82a4bcf55935f0801b14bca6be9c71e32ae914): global forwarding callback-before-reply, missing-callback rejection and cancel reply framing.
- [PR919 commit 0317c40fc131fab952d291d43c56c7b7ce5f4303](https://github.com/wolfSSL/wolfssh/commit/0317c40fc131fab952d291d43c56c7b7ce5f4303): reject direct-tcpip channel without forwarding callback.
`review.py` independently reverses only the new notice, the two initial handler
changes and nine exact channel-name predicates, then requires the complete prior
generated-source hash. The complete original `DoChannelRequest` is recovered by
reversing just those predicates. Any other source change, including ordering,
client parsing, request branch bodies, password wiping or crypto callers, fails
this fence. Original/version/anchor guards remain;
no silent repin or removal of existing strict build-source checks.
## PR899: distinguish client host keys from server authentication
`ParseRSAPubKey` reads an unchecked algorithm-name length then adds it to its index.
`ParseECCPubKey` does the same for the curve name. An oversized/wrapping length can
invalidate the intended cursor progression. PR899 replaces these skips with
`GetSkip`; it does not establish full curve-name/key-blob semantic validation.
These are real dependency defects, not evidence of current server exploitation.
Exact static call chain: `DoKexDhReply``ParsePubKey``ParseRSAPubKey` or
`ParseECCPubKey`. There is one call to `ParsePubKey`, in `DoKexDhReply`. The current
server user-key verifier instead uses `DoUserAuthRequestPublicKey`
`DoUserAuthRequestEcc`/`DoUserAuthRequestEd25519`; it does not call either PR899 key
parser. Application authorization additionally validates and exactly matches the
stored key blob before successful proof-of-possession verification.
This exclusion is **not just a server-role assertion**:
1. `src/ssh_transport.c` creates only `WOLFSSH_ENDPOINT_SERVER` contexts.
2. Generated `DoPacket` checks `IsMessageAllowed(..., WS_MSG_RECV)` before dispatch.
3. The current two KEX choices set the server expectation to `MSGID_KEXDH_INIT`
(30), then `MSGID_NEWKEYS` (21), never `MSGID_KEXDH_REPLY` (31) or GEX reply (33).
Both initial KEX and rekey use those restrictions. Before peer KEXINIT or with
no nonzero matching expectation, reply messages are rejected as well.
4. Both dispatch routes to `DoKexDhReply` (31 and 33) therefore fail the generated
expectation gate. Disabled DH/GEX must not be confused with the compiled case
labels. The full generated ordering suite was rerun, including wrong-message
rejection and both KEX exchanges/rekeys.
5. Production macro replay confirms `WOLFSSH_NO_RSA`, `WOLFSSH_NO_DH`, and disabled
certificates. Client code is not generally compile-disabled: ECC client parser
safety depends on the role/ordering contract, not on dead-code assumptions.
**Disposition:** retain both key parsers byte-identical, fence the call chain and
ordering source, and require this decision to be revisited before enabling client
use or widening KEX. No standalone client parsing safety claim.
### CHANNEL_FAILURE correction and behavior
The pinned `DoChannelFailure` did not read a recipient at all: `len != 0` returned
`WS_BAD_ARGUMENT`; an empty payload returned `WS_CHANOPEN_FAILED`. It did not have
the out-of-bounds read implied by blindly treating it as the newer parser. But
CHANNEL_FAILURE is a connection-protocol message allowed after authentication by
the current server gate, so its malformed-input contract is relevant even though
the application has no useful outstanding channel-request workflow needing it.
The local adaptation validates pointers, uses `GetUint32` on a local cursor,
requires exactly one remaining recipient field (`begin == len`), and verifies the
recipient through `ChannelFind(..., WS_CHANNEL_ID_SELF)`. Only then does it publish
the cursor and return the existing `WS_CHANOPEN_FAILED`. Truncation/wrapping offsets
and trailing data return `WS_BUFFER_E`; an unknown recipient returns
`WS_INVALID_CHANID`. Failure leaves the caller index unchanged; no channel/session
state is mutated. No new queue, allocation, retry or callback is introduced.
This **does not turn CHANNEL_FAILURE into a recoverable reply** or implement
request correlation. Both old nonempty rejection and new parsed failure remain
fatal to this application's worker path, which treats only its explicit
would-block/receive statuses as retryable. The correction establishes bounded
framing and the appropriate existing failure result, not an authentication-bypass
or memory-corruption exploit fix. It is a local adaptation, not a full PR899 patch.
## PR902 and present channel callback policy
The old bounded `DoServiceRequest` accepted any short service string and advanced
to `CLIENT_USERAUTH_REQUEST_DONE`. PR902 really is applicable before user
authentication: ordering permits SERVICE_REQUEST at `ACCEPT_KEYED` but does not
validate its name. The generated handler now requires length 12 and exact bytes
`ssh-userauth`; mismatch returns `WS_INVALID_STATE_E` before index/state publication.
Length comparison short-circuits before the fixed-span comparison. Existing bounds
and the strict name-capacity limit remain. Unlike upstream's later-tree patch,
this subset does not queue a best-effort disconnect: the owner already closes on
this error. Valid-service transition is unchanged. `DoServiceAccept` is unchanged;
the current server gate rejects SERVICE_ACCEPT before dispatch.
For the channel/forwarding question:
- Actual Xtensa replay confirms `WOLFSSH_FWD`, `WOLFSSH_AGENT`, `WOLFSSH_CERTS`,
`WOLFSSH_SFTP`, and `WOLFSSH_SCP` absent. PR918's `DoGlobalRequestFwd` call sites
and PR919's direct-tcpip handling are under `WOLFSSH_FWD`. Unsupported forwarding
channel types take the default unknown-type failure before channel allocation;
global forwarding requests fall through to failure if a reply is requested.
- `create_context` registers shell, exec and subsystem callbacks, not a channel-open
or global-request callback. Default session-channel acceptance is intentional:
the pinned handler limits it to one channel, and auth ordering precedes it.
- Shell callback marks `shell_requested`; exec/subsystem callbacks reject.
`process_handshake` additionally requires an authenticated/current principal,
that flag and `WOLFSSH_SESSION_SHELL` before broker/admin routing. Callback
rejection alone is not the whole policy: the library stores session type and
completion state even for rejected requests, while the application gate stops
exec/subsystem admission. The registered callbacks never execute commands.
- The pinned generic channel-open callback rejection path appends the channel even
after callback failure; no callback is installed here, so that dormant path is
not patched by this review. Revisit before adding one. Do not infer that PR919
repairs generic channel-open callbacks; its archived hunk is forwarding-only.
### Completed follow-up: exact channel-request names
The concrete prefix issue is **closed**, without refactoring the request parser.
All nine `WSTRNCMP(type, literal, typeSz) == 0` predicates are replaced by
`typeSz == sizeof(literal) - 1 && WMEMCMP(type, literal, sizeof(literal) - 1) == 0`.
The length check short-circuits before any comparison on a short name. `memcmp`
compares through embedded NULs instead of accepting a terminated prefix. Existing
bounded `GetString` copies at most 31 bytes; every recognized name is shorter, so
an oversized name truncated to 31 bytes cannot alias a recognized name. No new
allocation, helper, protocol response, state transition or feature setting.
The full handler and application gates were rechecked, not just the shell branch:
| Exact name | Existing branch / actual production gate |
| --- | --- |
| `env` | Parses two strings; no environment-setting callback. Always compiled. |
| `shell` | Sets shell session type, calls registered `accept_shell`, marks library completion. Application still requires the callback's `shell_requested` flag and shell session type. |
| `exec`, `subsystem` | Parse command, store their session type, call registered rejecting callbacks. Library completion is not application admission; no command is executed by these callbacks. |
| `pty-req` | Under `WOLFSSH_TERM`, **present** in production. Parses term/dimensions/modes; resize callback is optional and not installed by this application. Does not authorize a shell. |
| `window-change` | Requires both `WOLFSSH_TERM` and `WOLFSSH_SHELL`; **absent** because production has no `WOLFSSH_SHELL`. Remains on the unknown path in that profile. |
| `exit-status`, `exit-signal` | Under TERM or SHELL; **present** via TERM. Existing payload parsing preserved. |
| `auth-agent-req@openssh.com` | Under `WOLFSSH_AGENT`; **absent**. Optional enabled-branch comparison tested only in a host fixture, not enabled in firmware. |
Empty names, proper prefixes, same-prefix suffixes, same-length wrong bytes,
embedded NULs and overlong names no longer select any recognized branch. They take
the **unchanged unknown-request path**: no branch callback/session-type update,
consume the payload and return success (send channel success if requested).
This deliberately does not introduce unknown-request rejection or strict trailing
payload validation. Malformed header/name/boolean framing still fails before
lookup/callback. Existing exec/subsystem behavior of calling their rejecting
callbacks even after a command-payload parse error is also preserved; those real
callbacks cannot execute commands. A prior accepted shell does not authorize a
later exec/subsystem: the actual application session-type gate still rejects it,
with or without a requested reply. No unauthenticated route is introduced.
`channel_request.c` executes actual generated helpers and the complete handler.
`channel_request.py` separately hash-pins and extracts the real `accept_shell`,
`reject_channel_request` and complete `process_handshake` bodies, checks their
registration/context wiring, and executes them with platform/routing doubles.
Tests exercise both broker/admin shell admission and rejection for missing callback
context/flag, missing authentication/principal, stale principal, non-shell session
and unsupported role. This is not a live broker/admin or task-lifecycle test.
## Generic signature API: finite caller closure, not library closure
Rechecked exact pinned `signature.c` and generated SSH calls, supplementing the
[key-validation trace](ssh_key_validation_review.md#raw-signatures-and-cve-2026-5194-applicability):
- `wc_SignatureVerifyHash` and `wc_SignatureGenerateHash_ex` reject zero sizes and
invalid hash types but do **not** require the supplied hash length to equal the
algorithm's digest length. That generic weakness remains; no global PR10131
backport or crypto configuration change is made.
- Current server ECC authentication is the sole enabled SSH
`wc_SignatureVerifyHash` caller. `DoUserAuthRequestPublicKey` derives the digest
size from `HashForId(pkTypeId)` and `wc_HashGetDigestSize`, checks errors, hashes
locally, then passes it to `DoUserAuthRequestEcc`. Authorized P256 implies SHA256,
32 bytes. A peer signature field does not supply this digest length. The other
SSH VerifyHash call is certificate-gated and absent.
- `SignHEcdsa` hashes exchange H locally using the negotiated P256 host-key hash and
calls `wc_ecc_sign_hash` with the full 32-byte digest. Ed25519 authentication uses
streamed message verification, not generic prehash verification.
- Both `wc_SignatureVerify` call sites are in blocked client `DoKexDhReply`; that
wrapper also derives/hashes a full digest internally. Client ECC auth signing
and certificate signing are not current server paths; agent signing is disabled.
No application `src/` call to generic signature generation/verification APIs or
raw `wc_ecc_sign_hash`/`wc_ecc_verify_hash` was found outside these vendor paths.
- No wolfSSL TLS context/connect/accept use was found in application `src/`;
HTTPS uses mbedTLS. This is application reachability evidence, **not** a claim
that wolfSSL TLS or generic ASN/signature APIs are compiled out or fixed.
**Closed question:** no short-digest/OID-confusion trigger in this inspected current
SSH caller set. **Reopen on:** certificate/client/agent enablement, new raw API
callers, key/KEX widening or a changed authorization/hash construction. General
wolfSSL TLS/ASN/API auditing remains outside this finite scope.
## Validation and remaining handoff
Executed in this slice:
| Command (all prefixed `CCACHE_DISABLE=1`) | Result |
| --- | --- |
| `python3 tests/wolfssh_parser_contract/run.py` | PASS: existing 3,258 cases × two stack modes / 11 mutations, plus channel matrix below / 20 additional rejected mutations; independent full-source/provenance fences. |
| `python3 tests/wolfssh_parser_contract/review.py --profile` | PASS: actual saved Xtensa feature replay and fresh-source syntax. Explicitly reports production input is the reviewed **prior** baseline. |
| `python3 tests/wolfssh_auth_contract/run.py --host-only` | PASS: 135 password/control-flow/wipe cases. |
| `python3 tests/sdk_security_overrides/run.py` | PASS: generator and CMake fixtures, including existing SDK corrections. No actual build-registration option used. |
| `python3 tests/wolfssh_order_contract/run.py` | PASS: 8,028 full-generated-source/real-crypto checks and seven rejected mutations. No OpenSSH interop option used. |
| `python3 tests/wolf_crypto_policy/run.py --host-only` (initial review, not rerun for name-only follow-up) | PASS: 20 guards, PUBLIC CMake fixture, real vendor small-math/P256/ASN vectors. No strict production crypto rerun. |
| `python3 tests/ssh_protocol_policy/run.py` (initial review; not rerun while build remains stale) | **Blocked as expected:** `Generated wolfSSH source differs from render_entry; reconfigure the build`. Its strict guard was not changed or bypassed. |
Follow-up channel matrix: **2,737 cases per stack mode** for production TERM-only,
TERM+SHELL, and TERM+SHELL+AGENT profiles; **2,735 per stack mode** for no-terminal
and SHELL-only profiles. All five profiles run both stack modes with guard pages
and UBSan trap instrumentation. The alternative features are host-only coverage,
not production settings. Tests cover every proper prefix, valid names, appended
bytes/NUL suffixes, every embedded-NUL/same-length wrong-byte position, 3165-byte
names, every packet/payload truncation, oversized/wrapping declared lengths,
nonzero offsets, want-reply both ways, known/unknown channels, PTY callbacks and
real application admission. Instrumented comparison asserts that the compared
span equals the initialized name length. **18 name/length mutations and two real
application shell-admission gate mutations are rejected**, in addition to the
existing 11 parser mutations. Ordering/auth/SDK suites and Xtensa profile/syntax
were rerun successfully after the follow-up; `git diff --check` also passed.
The new parser tests use crypto/channel doubles; they establish dispatch/gating
and preserved state contracts, not cryptographic arithmetic or actual channel lifetime. The
ordering suite supplies separate full-library host evidence. No resource/timing,
firmware link, device, broad fuzzing, new network SSH or Phase9 acceptance claim.
Remaining handoff is bounded:
1. Parent-approved regeneration/build and strict production-source suites after
integrating concurrent work; production generated bytes are deliberately stale.
2. The requested channel-name dispatch and callback-gate review is **complete**.
Existing unknown/trailing-payload behavior is explicitly preserved, not certified
as a generally strict parser and not expanded into another parser inventory.
3. Existing whole-phase hardware/rekey cleanup, compatibility and heap/stack/CPU
gates remain as recorded in the ordering/key-validation reviews. No new target
cost measurement is claimed for these allocation-free checks.
4. Dormant client-key/forwarding/certificate/generic-API defects are documented
profile exclusions, not fixed dependency features. Re-audit only if those
capabilities or the pinned source/role/ordering contract change.
+5 -5
View File
@@ -4,13 +4,13 @@
**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 (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.
**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. The [finite remaining SSH review](ssh_parser_remaining_review.md) is complete: bounded exact CHANNEL_FAILURE recipient parsing (fatal policy retained), exact `ssh-userauth` service validation, and exact length/byte dispatch for all nine channel-request names are implemented. Unknown-request/trailing-payload behavior is preserved. PR899 client key skips are unchanged and blocked by current server role/ordering; PR918/919 forwarding is disabled. The inspected generic signature caller trace found no attacker-selected short-digest/OID path; generic APIs remain unpatched. These are profile-specific dispositions, not exhaustive parser/library safety; revisit on caller, feature, KEX or source changes.
**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 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.
Supplied parent build evidence: `pio run` **PASS**, **94,340 B linked RAM / 1,768,901 B flash**, unchanged RAM / **+200 B flash** versus 1,768,701 B. Final parent results: **all seven 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, notices (30), and parser (3,258 cases × two modes plus channel profiles; 11 + 18 + 2 rejected mutations). 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.
**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.
**Restricted ordering correction implemented for CVE-2025-14942; not a full upstream backport or advisory/phase sign-off.** The finite parser/client/generic-caller questions are dispositioned in the linked report; broader advisory coverage, whole-phase target validation and [release delivery/source/legal gates](release_packaging.md#separate-work-before-distribution) remain open. Offline notice assembly is implemented, not distribution clearance. No dependency upgrade or device operation was performed. Phase 9 is not complete or production-ready.
### Restricted ordering correction and remaining gates
@@ -26,11 +26,11 @@ The initial host interoperability run failed a close race despite printing `INTE
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.
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.
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. Broader advisory coverage and release-specific source/legal/recipient delivery remain open; finite parser review and notice assembly are no longer pending implementation. 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, ECC/X25519 and ordering status claims. Restricted ordering is implemented; its target/compatibility gates 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 remain open; the [finite remaining parser review](ssh_parser_remaining_review.md) supersedes historical deferred PR899/service/client/generic-caller questions, without certifying dormant dependency code.**
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.
+17
View File
@@ -0,0 +1,17 @@
# Release notice tool tests
Run `python3 tests/release_notices/run.py` from the repository root. Uses Python's
standard library and isolated temporary fixtures only; no managed package,
SDK, toolchain, PlatformIO, network, or device is required. Linux/POSIX path and
descriptor semantics match the notice tool.
Covers exact full-text/excerpt preservation and manifests; deterministic bytes
across moved roots/changed mtimes; missing, empty, changed and oversized inputs;
source body drift outside excerpts; bounds/schema; traversal and symlinks in
input/output ancestry; FIFOs/directories; existing user-data preservation;
explicit output requirement; unlisted secret/config/build exclusion; incomplete
write behavior; and success/failure CLI exits.
Real installed-input assembly and recipient delivery are separate checks; see
`docs/release_packaging.md`. Passing these tests is not license clearance or
proof of corresponding-source compliance.
+296
View File
@@ -0,0 +1,296 @@
#!/usr/bin/env python3
# SPDX-License-Identifier: GPL-3.0-only
"""Temporary-fixture contract tests; no installed dependencies or device required."""
import copy
import importlib.util
import json
import os
from pathlib import Path
import subprocess
import sys
import tempfile
import unittest
from unittest import mock
sys.dont_write_bytecode = True
PROJECT = Path(__file__).absolute().parents[2]
SPEC = importlib.util.spec_from_file_location("release_notices", PROJECT / "tools/release_notices.py")
notices = importlib.util.module_from_spec(SPEC)
SPEC.loader.exec_module(notices)
class BundleTests(unittest.TestCase):
def setUp(self):
self.temp = tempfile.TemporaryDirectory(prefix="release-notices-test-")
self.addCleanup(self.temp.cleanup)
self.base = Path(self.temp.name)
self.roots = {name: self.base / name for name in notices.ROOTS}
for root in self.roots.values():
root.mkdir()
self.catalog = {"schema": 1, "snapshot": {"fixture": "1"}, "inputs": []}
self.add_input("project", "LICENSE", b"Full license\nCopyright holder\n")
self.add_input("sdk", "nested/COPYING", b"First grant\nSecond grant\nDisclaimer\n")
self.add_input("toolchain", "source.c", b"/* full notice */\nint code;\n", [0, 18])
self.output = self.base / "bundle"
def add_input(self, root, path, data, span=None):
target = self.roots[root] / path
target.parent.mkdir(parents=True, exist_ok=True)
target.write_bytes(data)
payload = data if span is None else data[span[0]:span[1]]
self.catalog["inputs"].append({
"root": root, "path": path, "size": len(data),
"sha256": notices.digest(data), "range": span,
"output_sha256": notices.digest(payload), "purpose": "test fixture",
})
def run_bundle(self, output=None, catalog=None):
return notices.assemble(self.roots, output or self.output,
notices.json_bytes(catalog or self.catalog))
def assert_preflight_failure(self):
with self.assertRaises((notices.NoticeError, OSError)):
self.run_bundle()
self.assertFalse(self.output.exists())
@staticmethod
def contents(root):
return {p.relative_to(root).as_posix(): p.read_bytes()
for p in root.rglob("*") if p.is_file()}
def test_exact_bytes_and_manifest(self):
manifest = self.run_bundle()
self.assertEqual(json.loads((self.output / "manifest.json").read_bytes()), manifest)
for entry in manifest["inputs"]:
data = (self.output / entry["output"]).read_bytes()
self.assertEqual(notices.digest(data), entry["output_sha256"])
self.assertEqual(len(data), entry["output_size"])
self.assertEqual((self.output / "inputs/sdk/nested/COPYING").read_bytes(),
b"First grant\nSecond grant\nDisclaimer\n")
self.assertEqual((self.output / "inputs/toolchain/source.c.notice.txt").read_bytes(),
b"/* full notice */\n")
self.assertNotIn(str(self.base), (self.output / "manifest.json").read_text())
self.assertEqual(self.output.stat().st_mode & 0o777, 0o700)
def test_deterministic_order_paths_and_mtime(self):
self.run_bundle()
first = self.contents(self.output)
# Moving the input roots and changing source mtimes must not affect bytes.
for name, root in list(self.roots.items()):
moved = self.base / (name + "-moved")
root.rename(moved)
self.roots[name] = moved
for file in moved.rglob("*"):
os.utime(file, (123456789, 123456789))
self.run_bundle(self.base / "second")
self.assertEqual(first, self.contents(self.base / "second"))
def test_missing_source(self):
(self.roots["sdk"] / "nested/COPYING").unlink()
self.assert_preflight_failure()
def test_changed_source_same_size(self):
path = self.roots["project"] / "LICENSE"
path.write_bytes(b"x" * path.stat().st_size)
self.assert_preflight_failure()
def test_changed_non_notice_source_body(self):
path = self.roots["toolchain"] / "source.c"
path.write_bytes(path.read_bytes().replace(b"code", b"evil"))
self.assert_preflight_failure()
def test_empty_source(self):
(self.roots["project"] / "LICENSE").write_bytes(b"")
self.assert_preflight_failure()
def test_growing_source(self):
path = self.roots["project"] / "LICENSE"
with path.open("ab") as stream:
stream.write(b"unexpected")
self.assert_preflight_failure()
def test_existing_directory_never_modified(self):
self.output.mkdir()
marker = self.output / "user-data"
marker.write_bytes(b"keep me")
with self.assertRaises(FileExistsError):
self.run_bundle()
self.assertEqual(self.contents(self.output), {"user-data": b"keep me"})
def test_existing_empty_directory_rejected(self):
self.output.mkdir()
with self.assertRaises(FileExistsError):
self.run_bundle()
self.assertEqual(list(self.output.iterdir()), [])
def test_existing_file_never_modified(self):
self.output.write_bytes(b"user data")
with self.assertRaises(FileExistsError):
self.run_bundle()
self.assertEqual(self.output.read_bytes(), b"user data")
def test_output_symlink_rejected(self):
self.output.symlink_to(self.base / "absent")
with self.assertRaises(FileExistsError):
self.run_bundle()
self.assertTrue(self.output.is_symlink())
self.assertFalse((self.base / "absent").exists())
def test_output_parent_symlink_rejected(self):
link = self.base / "link"
link.symlink_to(self.base, target_is_directory=True)
with self.assertRaises(OSError):
self.run_bundle(link / "bundle")
self.assertFalse(self.output.exists())
def test_output_inside_inputs_rejected(self):
for root in self.roots.values():
with self.subTest(root=root), self.assertRaises(notices.NoticeError):
self.run_bundle(root / "bundle")
self.assertFalse((root / "bundle").exists())
def test_parent_traversal_output_rejected(self):
with self.assertRaises(notices.NoticeError):
self.run_bundle(self.base / "project/../bundle")
self.assertFalse(self.output.exists())
def test_missing_parent_not_created(self):
with self.assertRaises(FileNotFoundError):
self.run_bundle(self.base / "absent/bundle")
self.assertFalse((self.base / "absent").exists())
def test_source_file_symlink_even_to_identical_bytes_rejected(self):
path = self.roots["project"] / "LICENSE"
other = self.base / "other"
path.rename(other)
path.symlink_to(other)
self.assert_preflight_failure()
def test_source_directory_symlink_rejected(self):
path = self.roots["sdk"] / "nested"
other = self.base / "other"
path.rename(other)
path.symlink_to(other, target_is_directory=True)
self.assert_preflight_failure()
def test_input_root_symlink_rejected(self):
root = self.roots["sdk"]
other = self.base / "other"
root.rename(other)
root.symlink_to(other, target_is_directory=True)
self.assert_preflight_failure()
def test_fifo_does_not_block(self):
path = self.roots["project"] / "LICENSE"
path.unlink()
os.mkfifo(path)
self.assert_preflight_failure()
def test_directory_is_not_a_notice(self):
path = self.roots["project"] / "LICENSE"
path.unlink()
path.mkdir()
self.assert_preflight_failure()
def test_catalog_paths_rejected(self):
for path in ("../secret", "/etc/passwd", "nested/../../secret", "a//b",
"./LICENSE", "a\\b", "", "a/./b", "a\x00b"):
with self.subTest(path=path):
catalog = copy.deepcopy(self.catalog)
catalog["inputs"][0]["path"] = path
with self.assertRaises(notices.NoticeError):
self.run_bundle(catalog=catalog)
self.assertFalse(self.output.exists())
def test_invalid_catalog_entries(self):
for field, value in (("root", "unknown"), ("size", 0),
("size", notices.MAX_FILE + 1), ("size", True),
("sha256", "wrong"), ("range", [-1, 2]),
("range", [0, 99999]), ("range", [2, 1]),
("output_sha256", "0" * 64)):
with self.subTest(field=field, value=value):
catalog = copy.deepcopy(self.catalog)
catalog["inputs"][0][field] = value
with self.assertRaises(notices.NoticeError):
self.run_bundle(catalog=catalog)
self.assertFalse(self.output.exists())
def test_duplicate_input(self):
self.catalog["inputs"].append(self.catalog["inputs"][0])
self.assert_preflight_failure()
def test_entry_count_bound(self):
self.catalog["inputs"] *= notices.MAX_ENTRIES
self.assert_preflight_failure()
def test_total_size_bound(self):
with mock.patch.object(notices, "MAX_TOTAL", 1):
self.assert_preflight_failure()
def test_unlisted_secrets_builds_and_configs_never_read(self):
for name in ("sdkconfig", ".env", "device.pem", ".pio/build/firmware.bin",
"backups/credentials.json"):
path = self.roots["project"] / name
path.parent.mkdir(parents=True, exist_ok=True)
path.write_bytes(b"SECRET_DO_NOT_COPY")
original = notices.read_bounded
calls = []
def tracked(fd, path, limit=notices.MAX_FILE):
calls.append(path)
return original(fd, path, limit)
with mock.patch.object(notices, "read_bounded", side_effect=tracked):
self.run_bundle()
self.assertEqual(set(calls), {e["path"] for e in self.catalog["inputs"]})
self.assertFalse(any(b"SECRET_DO_NOT_COPY" in b for b in self.contents(self.output).values()))
def test_failed_write_has_no_completion_marker_or_cleanup(self):
original = notices.write_new
def fail(fd, path, data):
if path == "README.txt":
raise OSError("injected write failure")
original(fd, path, data)
with mock.patch.object(notices, "write_new", side_effect=fail):
with self.assertRaises(OSError):
self.run_bundle()
self.assertTrue(self.output.exists())
self.assertFalse((self.output / "manifest.json").exists())
# Retrying cannot overwrite or delete even this partial output.
with self.assertRaises(FileExistsError):
self.run_bundle()
def test_binary_and_non_utf8_output_rejected(self):
for data in (b"binary\x00notice", b"invalid\xffnotice"):
with self.subTest(data=data):
self.catalog["inputs"] = []
self.add_input("project", "bad", data)
self.assert_preflight_failure()
def test_cli_requires_explicit_output(self):
result = subprocess.run([sys.executable, str(PROJECT / "tools/release_notices.py"),
"--sdk-root", str(self.roots["sdk"]),
"--toolchain-root", str(self.roots["toolchain"])],
capture_output=True, timeout=10)
self.assertEqual(result.returncode, 2)
self.assertIn(b"--output", result.stderr)
def test_cli_with_isolated_policy_and_sources(self):
policy = self.base / "policy"
(policy / "tools").mkdir(parents=True)
(policy / "third_party/release-notices").mkdir(parents=True)
script = policy / "tools/release_notices.py"
script.write_bytes((PROJECT / "tools/release_notices.py").read_bytes())
(policy / notices.CATALOG).write_bytes(notices.json_bytes(self.catalog))
command = [sys.executable, str(script), "--project-root", str(self.roots["project"]),
"--sdk-root", str(self.roots["sdk"]), "--toolchain-root",
str(self.roots["toolchain"]), "--output", str(self.output)]
result = subprocess.run(command, capture_output=True, timeout=10)
self.assertEqual(result.returncode, 0, result.stderr)
before = self.contents(self.output)
result = subprocess.run(command, capture_output=True, timeout=10)
self.assertEqual(result.returncode, 1)
self.assertEqual(before, self.contents(self.output))
if __name__ == "__main__":
unittest.main(verbosity=2)
+98 -17
View File
@@ -4,6 +4,7 @@ Run from the project root (installed pinned sources and a host C compiler requir
```sh
CCACHE_DISABLE=1 python3 tests/wolfssh_parser_contract/run.py
CCACHE_DISABLE=1 python3 tests/wolfssh_parser_contract/review.py --profile
CCACHE_DISABLE=1 python3 tests/wolfssh_auth_contract/run.py --host-only
CCACHE_DISABLE=1 python3 tests/sdk_security_overrides/run.py
```
@@ -17,7 +18,47 @@ run with guard pages and UBSan trap instrumentation, both with and without
CMake source replacement using fixtures. This is **not** a claim that an existing
production generated file or firmware binary contains these edits.
## Reviewed upstream evidence and exact implementation scope
## 2026-09-16 remaining-parser review
The [finite applicability review](../../docs/ssh_parser_remaining_review.md) closes
PR899's current-profile disposition and corrects the earlier description of PR902:
902 is **service-name validation**, not channel-callback hardening.
- PR899 client RSA/ECC key-skip hunks remain unapplied: RSA is disabled, and the
sole client host-key call chain is blocked by current server KEX ordering.
- The current `DoChannelFailure` now bounds exactly one recipient ID, verifies the
local channel, and consumes it before returning the existing fatal
`WS_CHANOPEN_FAILED`. No state or channel mutation is introduced.
- The server subset of PR902 rejects every name except exact `ssh-userauth` before
publishing the index/state transition. The owner closes on the error; unlike
upstream, no best-effort disconnect packet is queued. Client service acceptance
stays unchanged and unreachable in the current server role.
- PR918/919 forwarding fixes are not applied with `WOLFSSH_FWD` disabled.
- The follow-up closes `DoChannelRequest` prefix/NUL aliases: all nine recognized
names require exact length **before** exact byte comparison. Supported branch
bodies and unknown-request success/ACK handling remain unchanged. Production
enables TERM but not SHELL/AGENT: PTY/exit branches are present, window-change and
agent branches absent. Disabled branches are tested separately without enabling
firmware features.
`pr899.patch`, `pr902.patch`, `pr918.patch`, `pr919.patch` and `provenance.json`
archive bytes fetched on 2026-09-16 and verified equal to the corresponding upstream
commit patches. Tests pin hashes, commits and URLs independently. `review.py`
reverses only the independently specified new notice, two initial handler deltas
and nine exact channel-name predicates and
requires the **whole prior original+ordering+parser generated-source hash**. This
fences unchanged client parsers, packet dispatch, crypto callers and request branch bodies without
normalizing away edits. The existing exact original hash and exact-once anchors
remain mandatory. No ordering delta or crypto configuration is changed.
`--profile` reads the unique saved production compile command, checks whether its
input is exactly the known prior baseline or fresh current source, replays real
Xtensa feature preprocessing, and syntax-checks a temporary fresh source. A prior
input is explicitly reported as **not regenerated**, never current-build evidence.
Existing strict build-registration suites still reject stale generated bytes.
No network or production build-tree writes occur in either test command.
## Earlier upstream evidence and retained implementation scope
Official diffs fetched and inspected on 2026-09-15:
@@ -39,8 +80,8 @@ Covered:
validates the full string with `GetSize` before `GetString`, retaining the old
strict `< WOLFSSH_MAX_NAMESZ` limit rather than upstream's truncation behavior.
Failure leaves the caller index and client state untouched. Successful state
transition remains exactly the old one; service-name semantic validation is
not added. The original `GetSize` already uses bounded subtraction and needs
transition remains exactly the old one for `ssh-userauth`; the 2026-09-16
addition above rejects other names. The original `GetSize` already uses bounded subtraction and needs
no change. `GetString` now uses it and rejects zero output capacity before
subtraction/copy; ordinary bounded truncation semantics remain unchanged.
- **881 subset:** `DoChannelWindowAdjust` rejects addition exceeding the 32-bit
@@ -77,22 +118,56 @@ both enabled in this server's reviewed profile. Advertisement is not treated as
- **892:** client `DoServiceAccept`, agent key preparation, daemon authentication,
Windows terminal changes. Password framing/wiping is the existing local
correction, intentionally not replaced with upstream's later formulation.
- **899:** no hunks applied. `ParseRSAPubKey`/`ParseECCPubKey` skips require separate
client/KEX reachability analysis (not the server's `DoUserAuthRequestEcc`).
The old `DoChannelFailure` does not parse a channel ID at all; changing only its
`len != 0` typo would not establish a bounded channel-ID parser. Its existing
behavior is left unchanged rather than claiming the later parser contract.
Windows port/terminal hunks are out of scope.
- **899:** unused client key skips and Windows port/terminal hunks are deliberately
not applied; their current-profile applicability is resolved in the new review,
not a claim that the dependency is fixed for clients. CHANNEL_FAILURE is covered
by the bounded local adaptation above.
- **880:** certificate RSA, agent, daemon, terminal, TPM and SCP changes are not
applied. No complete PR-880 closure is claimed.
- Message ordering/state machine (including CVE-2025-14942), service semantics,
standalone ECC curve-name/key-blob semantic validation, other parsers, client
- Message ordering/state machine (including CVE-2025-14942) has its separate
restricted-profile suite. Standalone ECC curve-name/key-blob semantic validation,
other parsers, client
behavior and broader crypto advisories are outside this slice. ECC point/import
validation belongs to the separate crypto-policy owner and is not changed here.
Existing account/key authorization, numeric r/s validity and Ed25519 raw
signature-size/crypto validity checks remain owned by their existing layers.
## Test boundaries
## Exact channel-request and application-gate tests
`channel_request.py` extracts complete generated `DoChannelRequest`, `GetBoolean`,
`GetUint32`, `GetSize`, `GetString`, `GetStringAlloc` and `GetStringRef` functions.
It independently hash-pins/extracts the actual application `accept_shell`,
`reject_channel_request` and complete `process_handshake`, and checks callback
registration and per-slot context wiring. `channel_request.c` runs those real
bodies with channel/context layouts, routing, allocation and platform doubles.
This is not a task/broker/console integration test.
Five feature profiles run in both stack modes with guard pages and UBSan traps:
production TERM-only, no-terminal, SHELL-only, TERM+SHELL and TERM+SHELL+AGENT.
Alternative flags affect host fixtures only. The matrix covers all nine names:
valid requests; every proper prefix (including empty); suffixes; embedded NULs
and same-length wrong bytes at every position; 3165-byte names; every header,
name, boolean and payload truncation; nonzero offsets; oversized/wrapping declared
lengths; reply/no-reply; invalid channel and send failure; resize callback absence
and failure. Instrumented comparisons assert the read length equals the initialized
name length, catching removed short-name guards even inside the accessible stack
buffer. Unknown/disabled names must not parse payload fields or invoke callbacks,
but retain existing success/ACK/consumption behavior.
Real callback/admission cases verify shell-only routing for both roles; null
callback context and absent shell callback; authentication/principal/currentness
requirements; rejected exec/subsystem after a prior shell, even with no reply;
and optional PTY callback behavior. Malformed exec/subsystem payloads still invoke
the existing rejecting callbacks and never become shell admission, matching the
unchanged handler bodies.
**2,737 cases per stack mode** for each TERM-containing profile, **2,735** for each
of the other two profiles. **18 prefix/length-guard mutations** and **two actual
application admission-gate mutations** are rejected, in addition to the existing
11 parser mutations. The independent whole-source reversal checks that only the
nine predicates change within `DoChannelRequest`, with no branch refactor.
## Earlier parser test boundaries
The C matrix exercises zero/truncated/exact/oversized/wrapping lengths, invalid
and nonzero offsets, zero-capacity output, copy canaries, window overflow boundary
@@ -115,15 +190,21 @@ covers both labels, all truncations, shortened/oversized/wrapping/trailing
signature strings, exact raw-signature forwarding and unchanged streamed message
bytes. Both paths retain crypto rejection behavior using doubles.
Validation: **3,124 cases per stack mode** (both pass with UBSan trap mode), plus
**six guard-removal mutations rejected**: ECC nested read bound, inner/outer exact
consumption, Ed25519 key/signature OR checks, and Ed25519 exact consumption. The
Validation: **3,258 cases per stack mode** (both pass with UBSan trap mode), plus
**11 guard-removal mutations rejected**: ECC nested read bound, inner/outer exact
consumption, Ed25519 key/signature OR checks, Ed25519 exact consumption, service
length/byte equality and channel-failure bounded read/exact end/known recipient.
`remaining.c` adds 134 counted cases, plus assertions for null arguments and an
unknown channel: all service truncations, prefix/suffix/embedded-NUL names,
nonzero offsets, channel truncation/trailing bytes, and wrapping indices. The
mutation copies exist only in temporary test files; core dumps are disabled for
those intentionally failing runs. These are framing-valid fixtures with crypto
doubles, not independently verified real signatures.
The runner also compares complete password, packet dispatch, public-key dispatch and selected deferred
functions against the pre-slice generated baseline to fence accidental changes.
The runner also compares complete password, packet dispatch, public-key dispatch and unused
key parsers against the pre-parser generated baseline to fence accidental changes.
The independent whole-source hash contract separately preserves the exact ordering
and all prior parser changes while allowing only this review's three corrections.
The separate auth suite executes its 135 password/control-flow cases, including
payload wipe, callback framing and asynchronous pending retention. No whole-library
fuzzing, real SSH exchange, firmware compile, hardware timing or security sign-off
@@ -0,0 +1,375 @@
/* SPDX-License-Identifier: GPL-3.0-only
* Execute extracted generated handler/helpers and real application callbacks /
* complete handshake admission. OS, channel, allocator and routing are doubles. */
#include <assert.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <unistd.h>
typedef uint8_t byte;
typedef uint32_t word32;
#define WS_SUCCESS 0
#define WS_BUFFER_E -1
#define WS_INVALID_CHANID -2
#define WS_MEMORY_E -3
#define WS_FATAL_ERROR -4
#define UINT32_SZ 4
#define BOOLEAN_SZ 1
#define WOLFSSH_MAX_NAMESZ 32
#define WS_CHANNEL_ID_SELF 0
#define WOLFSSH_SESSION_SHELL 1
#define WOLFSSH_SESSION_EXEC 2
#define WOLFSSH_SESSION_SUBSYSTEM 3
#define CLIENT_DONE 42
#define WLOG(...) ((void)0)
#define WMEMCPY memcpy
#define WSTRNCMP strncmp
#define WMALLOC(n,h,t) malloc(n)
#define WFREE(p,h,t) free(p)
typedef struct { int sessionType; char *command; } WOLFSSH_CHANNEL;
typedef struct {
void *heap;
int (*channelReqShellCb)(WOLFSSH_CHANNEL*, void*);
int (*channelReqExecCb)(WOLFSSH_CHANNEL*, void*);
int (*channelReqSubsysCb)(WOLFSSH_CHANNEL*, void*);
void *agentCb;
} Context;
typedef struct WOLFSSH {
Context *ctx;
void *channelReqCtx, *termCtx;
int clientState, useAgent;
byte *modes;
word32 modesSz, widthChar, heightRows, widthPixels, heightPixels, exitStatus;
int (*termResizeCb)(struct WOLFSSH*, word32, word32, word32, word32, void*);
} WOLFSSH;
static WOLFSSH_CHANNEL channel;
static unsigned finds, replies, shell_calls, exec_calls, subsys_calls, resizes, reads32, cases;
static int reply_ok, reply_error, resize_error;
static word32 expected_type_length;
static int compare_type(const void *a, const void *b, size_t n)
{
/* This also detects an unguarded read of the uninitialized short-name tail
* inside the handler's accessible local 32-byte buffer. */
assert(n == expected_type_length);
return memcmp(a,b,n);
}
#define WMEMCMP compare_type
static void ato32(const byte *p, word32 *v)
{ reads32++; *v=((word32)p[0]<<24)|((word32)p[1]<<16)|((word32)p[2]<<8)|p[3]; }
static void put(byte *p, word32 v)
{ p[0]=v>>24; p[1]=v>>16; p[2]=v>>8; p[3]=v; }
static WOLFSSH_CHANNEL *ChannelFind(WOLFSSH *ssh, word32 id, int side)
{ assert(side==WS_CHANNEL_ID_SELF); finds++; return id==7?&channel:NULL; }
static int SendChannelSuccess(WOLFSSH *ssh, word32 id, int success)
{ replies++; reply_ok=success; return reply_error; }
#include "channel_actual.c"
/* Minimal external dependencies for the complete real process_handshake body. */
typedef int esp_err_t;
#define ESP_OK 0
#define ESP_ERR_INVALID_STATE -1
#define SSH_TRANSPORT_WOLFSSH_READ_BUDGET 99
#define USER_ROLE_USER 1
#define USER_ROLE_ADMIN 2
#define SSH_TRANSPORT_ROUTE_BROKER 1
#define SSH_TRANSPORT_ROUTE_ADMIN_CONSOLE 2
#define SSH_TRANSPORT_SESSION_ACTIVE 3
typedef struct { int role; } Principal;
typedef struct {
WOLFSSH *ssh;
bool shell_requested, authenticated, principal_valid;
Principal principal;
int64_t handshake_deadline_us, last_reconcile_us;
unsigned io_read_budget;
size_t console_slot_index;
int route, state;
} ssh_slot_t;
typedef struct { size_t slot_index; } admin_ssh_console_token_t;
static struct {
unsigned request_rejections, handshake_timeouts, broker_failures,
admin_console_admission_failures, admin_console_admissions,
handshake_successes, handshake_failures;
} s_counters;
static int s_admin_console_owner;
static bool principal_is_current=true;
static unsigned closes, broker_routes, admin_routes;
static void add_counter(unsigned *p, unsigned n) { *p+=n; }
static int64_t esp_timer_get_time(void) { return 1; }
static void request_slot_close(ssh_slot_t *s, bool revoked) { closes++; }
static int wolfSSH_accept(WOLFSSH *ssh) { return WS_SUCCESS; }
static int wolfSSH_GetSessionType(WOLFSSH *ssh) { return channel.sessionType; }
static esp_err_t user_database_principal_is_current(Principal *p, bool *current)
{ *current=principal_is_current; return ESP_OK; }
static esp_err_t connect_broker(ssh_slot_t *s, size_t i) { broker_routes++; return ESP_OK; }
static admin_ssh_console_token_t admin_console_token(ssh_slot_t *s, size_t i)
{ return (admin_ssh_console_token_t){i}; }
static esp_err_t admin_ssh_console_open_available(admin_ssh_console_token_t *t,
Principal *p, int *owner) { admin_routes++; return ESP_OK; }
static bool slot_principal_is_current(ssh_slot_t *s) { return principal_is_current; }
static void disconnect_failed_admission(ssh_slot_t *s) {}
static bool wolfssh_would_block(WOLFSSH *ssh, int result) { return false; }
#include "channel_application.c"
static int shell_cb(WOLFSSH_CHANNEL *c, void *s)
{ shell_calls++; assert(c==&channel); return accept_shell(c,s); }
static int exec_cb(WOLFSSH_CHANNEL *c, void *s)
{ exec_calls++; assert(c==&channel); return reject_channel_request(c,s); }
static int subsys_cb(WOLFSSH_CHANNEL *c, void *s)
{ subsys_calls++; assert(c==&channel); return reject_channel_request(c,s); }
static int resize_cb(WOLFSSH *s, word32 w, word32 h, word32 x, word32 y, void *ctx)
{ resizes++; assert(w==80 && h==24 && x==640 && y==480); return resize_error; }
enum { ENV, SHELL, EXEC, SUBSYS, PTY, WINDOW, STATUS, SIGNAL, AGENT, UNKNOWN };
static const char *names[]={"env","shell","exec","subsystem","pty-req",
"window-change","exit-status","exit-signal","auth-agent-req@openssh.com"};
static int enabled(int kind)
{
switch(kind) {
case ENV: case SHELL: case EXEC: case SUBSYS: return 1;
#ifdef WOLFSSH_TERM
case PTY: return 1;
#endif
#if defined(WOLFSSH_TERM) && defined(WOLFSSH_SHELL)
case WINDOW: return 1;
#endif
#if defined(WOLFSSH_TERM) || defined(WOLFSSH_SHELL)
case STATUS: case SIGNAL: return 1;
#endif
#ifdef WOLFSSH_AGENT
case AGENT: return 1;
#endif
default: return 0;
}
}
static word32 append_string(byte *p, const void *s, word32 n)
{ put(p,n); memcpy(p+4,s,n); return n+4; }
static word32 payload(byte *p, int kind)
{
word32 n=0;
if(kind==ENV) {
n+=append_string(p+n,"A",1); n+=append_string(p+n,"B",1);
} else if(kind==EXEC || kind==SUBSYS) {
n+=append_string(p+n,"cmd",3);
} else if(kind==PTY || kind==WINDOW) {
if(kind==PTY) n+=append_string(p+n,"xterm",5);
put(p+n,80); put(p+n+4,24); put(p+n+8,640); put(p+n+12,480); n+=16;
if(kind==PTY) n+=append_string(p+n,"\0",1);
} else if(kind==STATUS) {
put(p,123); n=4;
} else if(kind==SIGNAL) {
n+=append_string(p+n,"TERM",4); p[n++]=0;
n+=append_string(p+n,"msg",3); n+=append_string(p+n,"en",2);
}
return n;
}
static word32 packet(byte *p, const byte *name, word32 n, int kind, int want)
{
put(p,7); word32 size=4+append_string(p+4,name,n); p[size++]=(byte)want;
return size+payload(p+size,kind);
}
static void reset(WOLFSSH *ssh, Context *ctx, ssh_slot_t *slot)
{
free(channel.command); free(ssh->modes);
memset(&channel,0,sizeof(channel)); memset(ssh,0,sizeof(*ssh));
memset(ctx,0,sizeof(*ctx)); memset(slot,0,sizeof(*slot));
ctx->channelReqShellCb=shell_cb; ctx->channelReqExecCb=exec_cb;
ctx->channelReqSubsysCb=subsys_cb; ctx->agentCb=ctx;
ssh->ctx=ctx; ssh->channelReqCtx=slot; ssh->termResizeCb=resize_cb;
ssh->clientState=9; ssh->exitStatus=99;
slot->ssh=ssh; slot->authenticated=true; slot->principal_valid=true;
slot->principal.role=USER_ROLE_USER; slot->handshake_deadline_us=100;
finds=replies=shell_calls=exec_calls=subsys_calls=resizes=reads32=0;
closes=broker_routes=admin_routes=0; reply_ok=-1; reply_error=resize_error=0;
principal_is_current=true; memset(&s_counters,0,sizeof(s_counters));
}
static void unchanged(const WOLFSSH *ssh, const ssh_slot_t *slot)
{
assert(!shell_calls && !exec_calls && !subsys_calls && !resizes);
assert(!slot->shell_requested && channel.sessionType==0 && !channel.command);
assert(ssh->clientState==9 && !ssh->modes && !ssh->modesSz);
assert(!ssh->widthChar && !ssh->heightRows && !ssh->widthPixels && !ssh->heightPixels);
assert(ssh->exitStatus==99 && !ssh->useAgent);
}
static void run_name(byte *end, const byte *name, word32 n, int wire_kind,
int selected, int want, word32 offset)
{
byte frame[256]; memset(frame,0xcc,sizeof(frame));
word32 len=offset+packet(frame+offset,name,n,wire_kind,want);
byte *p=end-len; memcpy(p,frame,len);
WOLFSSH ssh={0}; Context ctx; ssh_slot_t slot; reset(&ssh,&ctx,&slot);
expected_type_length=n<32?n:31;
word32 idx=offset;
assert(DoChannelRequest(&ssh,p,len,&idx)==WS_SUCCESS);
assert(idx==len && finds==1 && replies==(unsigned)want);
assert(memcmp(p,frame,len)==0);
if(!enabled(selected)) {
unchanged(&ssh,&slot);
assert(reads32==2); /* No payload parser for unknown/disabled names. */
assert(!want || reply_ok==1); /* Preserve unknown-request policy. */
} else {
const unsigned extra_reads[]={2,0,1,1,6,4,1,3,0};
assert(reads32==2+extra_reads[selected]);
assert(shell_calls==(unsigned)(selected==SHELL));
assert(exec_calls==(unsigned)(selected==EXEC));
assert(subsys_calls==(unsigned)(selected==SUBSYS));
assert(!want || reply_ok==(selected!=EXEC && selected!=SUBSYS));
assert(slot.shell_requested==(selected==SHELL));
if(selected==SHELL || selected==EXEC || selected==SUBSYS) {
assert(ssh.clientState==CLIENT_DONE);
assert(channel.sessionType==(selected==SHELL?WOLFSSH_SESSION_SHELL:
selected==EXEC?WOLFSSH_SESSION_EXEC:WOLFSSH_SESSION_SUBSYSTEM));
if(selected!=SHELL) assert(strcmp(channel.command,"cmd")==0);
} else assert(ssh.clientState==9 && channel.sessionType==0);
if(selected==PTY || selected==WINDOW) {
assert(resizes==1 && ssh.widthChar==80 && ssh.heightRows==24);
if(selected==PTY) assert(ssh.modesSz==1 && ssh.modes[0]==0);
} else assert(!resizes);
assert(ssh.exitStatus==(selected==STATUS?123u:99u));
assert(ssh.useAgent==(selected==AGENT));
}
/* Actual application gate: even success/ACK is not shell admission. */
process_handshake(&slot,0);
assert(broker_routes==(unsigned)(selected==SHELL));
assert(closes==(unsigned)(selected!=SHELL));
free(channel.command); channel.command=NULL; free(ssh.modes);
cases++;
}
static void names_matrix(byte *end)
{
for(int k=ENV;k<UNKNOWN;k++) {
word32 n=(word32)strlen(names[k]); byte name[80]; memcpy(name,names[k],n);
for(int want=0;want<=1;want++) for(word32 off=0;off<=3;off+=3) {
run_name(end,name,n,k,k,want,off);
/* Every proper prefix, including the empty name. */
for(word32 j=0;j<n;j++) run_name(end,name,j,k,UNKNOWN,want,off);
name[n]='x'; run_name(end,name,n+1,k,UNKNOWN,want,off);
name[n]=0; name[n+1]='x'; run_name(end,name,n+2,k,UNKNOWN,want,off);
for(word32 j=0;j<n;j++) {
byte saved=name[j]; name[j]=0;
run_name(end,name,n,k,UNKNOWN,want,off); name[j]=saved;
name[j]='!'; run_name(end,name,n,k,UNKNOWN,want,off); name[j]=saved;
}
for(word32 size=31;size<=65;size++) {
memset(name+n,'x',size-n);
run_name(end,name,size,k,UNKNOWN,want,off);
}
}
}
}
static void truncations(byte *end)
{
for(int k=ENV;k<UNKNOWN;k++) {
word32 n=(word32)strlen(names[k]); byte frame[256];
word32 len=packet(frame,(const byte*)names[k],n,k,1);
for(word32 size=0;size<len;size++) {
byte *p=end-size; memcpy(p,frame,size);
WOLFSSH ssh={0}; Context ctx; ssh_slot_t slot; reset(&ssh,&ctx,&slot);
word32 idx=0; expected_type_length=n;
int ret=DoChannelRequest(&ssh,p,size,&idx);
if(size<9+n) {
assert(ret==WS_BUFFER_E && idx==0 && finds==0 && replies==0);
unchanged(&ssh,&slot);
} else if(enabled(k)) {
assert(ret==WS_BUFFER_E && idx==0 && replies==1 && !reply_ok);
/* Preserve old exec/subsystem callback invocation even if their
* payload parse failed; actual project callbacks only reject. */
assert(!shell_calls && exec_calls==(unsigned)(k==EXEC));
assert(subsys_calls==(unsigned)(k==SUBSYS) && !slot.shell_requested);
assert(!resizes && !ssh.modes);
} else {
assert(ret==WS_SUCCESS && idx==size && replies==1 && reply_ok);
unchanged(&ssh,&slot);
}
process_handshake(&slot,0);
assert(closes==1 && !broker_routes && !admin_routes);
free(channel.command); channel.command=NULL; free(ssh.modes); cases++;
}
}
/* Declared oversized/wrapping name lengths must fail before callbacks. */
const word32 bad[]={UINT32_MAX,UINT32_MAX-3,100};
for(unsigned j=0;j<3;j++) {
byte *p=end-9; put(p,7); put(p+4,bad[j]); p[8]=1;
WOLFSSH ssh={0}; Context ctx; ssh_slot_t slot; reset(&ssh,&ctx,&slot);
word32 idx=0; assert(DoChannelRequest(&ssh,p,9,&idx)==WS_BUFFER_E);
assert(idx==0 && !finds && !replies); unchanged(&ssh,&slot); cases++;
}
}
static void callback_gates(byte *end)
{
for(int k=SHELL;k<=SUBSYS;k++) for(int mode=0;mode<8;mode++) {
WOLFSSH ssh={0}; Context ctx; ssh_slot_t slot; reset(&ssh,&ctx,&slot);
byte frame[64]; word32 n=(word32)strlen(names[k]);
word32 len=packet(frame,(const byte*)names[k],n,k,1);
byte *p=end-len; memcpy(p,frame,len); word32 idx=0; expected_type_length=n;
if(mode==0) ssh.channelReqCtx=NULL;
if(mode==1) ctx.channelReqShellCb=NULL;
if(mode==2) slot.authenticated=false;
if(mode==3) slot.principal_valid=false;
if(mode==4) principal_is_current=false;
if(mode==5) slot.principal.role=USER_ROLE_ADMIN;
if(mode==6) slot.principal.role=0;
assert(DoChannelRequest(&ssh,p,len,&idx)==WS_SUCCESS);
if(k!=SHELL) assert(!reply_ok && !slot.shell_requested);
if(k==SHELL && mode==0) assert(!reply_ok && !slot.shell_requested);
process_handshake(&slot,0);
bool admit=k==SHELL && (mode==5 || mode==7);
assert(closes==(unsigned)!admit);
assert(broker_routes==(unsigned)(admit && mode==7));
assert(admin_routes==(unsigned)(admit && mode==5));
assert(slot.state==(admit?SSH_TRANSPORT_SESSION_ACTIVE:0));
free(channel.command); channel.command=NULL; free(ssh.modes); cases++;
}
/* A prior shell callback must not authorize a subsequent exec/subsystem,
* even without a requested rejection reply. Execute both real dispatches. */
for(int k=EXEC;k<=SUBSYS;k++) for(int want=0;want<=1;want++) {
WOLFSSH ssh={0}; Context ctx; ssh_slot_t slot; reset(&ssh,&ctx,&slot);
byte frame[64]; word32 len=packet(frame,(const byte*)"shell",5,SHELL,0);
byte *p=end-len; memcpy(p,frame,len); word32 idx=0; expected_type_length=5;
assert(DoChannelRequest(&ssh,p,len,&idx)==WS_SUCCESS && slot.shell_requested);
word32 n=(word32)strlen(names[k]);
len=packet(frame,(const byte*)names[k],n,k,want);
p=end-len; memcpy(p,frame,len); idx=0; expected_type_length=n;
assert(DoChannelRequest(&ssh,p,len,&idx)==WS_SUCCESS && slot.shell_requested);
assert(channel.sessionType!=WOLFSSH_SESSION_SHELL);
assert(replies==(unsigned)want && (!want || !reply_ok));
process_handshake(&slot,0);
assert(closes==1 && !broker_routes && !admin_routes);
free(channel.command); channel.command=NULL; free(ssh.modes); cases++;
}
/* Production does not install a terminal resize callback. Also preserve
* the original callback-error propagation when one is installed. */
if(enabled(PTY)) for(int mode=0;mode<2;mode++) {
WOLFSSH ssh={0}; Context ctx; ssh_slot_t slot; reset(&ssh,&ctx,&slot);
byte frame[128]; word32 len=packet(frame,(const byte*)"pty-req",7,PTY,1);
byte *p=end-len; memcpy(p,frame,len); word32 idx=0; expected_type_length=7;
if(mode==0) ssh.termResizeCb=NULL; else resize_error=WS_FATAL_ERROR;
assert(DoChannelRequest(&ssh,p,len,&idx)==(mode?WS_FATAL_ERROR:WS_SUCCESS));
assert(ssh.widthChar==80 && ssh.modesSz==1 && resizes==(unsigned)mode);
assert(!slot.shell_requested && !shell_calls && reply_ok==!mode);
assert(idx==(mode?0:len));
free(ssh.modes); cases++;
}
/* Existing unknown-recipient and send failure behavior is not refactored. */
for(int mode=0;mode<2;mode++) {
WOLFSSH ssh={0}; Context ctx; ssh_slot_t slot; reset(&ssh,&ctx,&slot);
byte frame[64]; word32 len=packet(frame,(const byte*)"shell",5,SHELL,1);
if(mode==0) put(frame,8); else reply_error=WS_FATAL_ERROR;
byte *p=end-len; memcpy(p,frame,len); word32 idx=0; expected_type_length=5;
assert(DoChannelRequest(&ssh,p,len,&idx)==(mode?WS_FATAL_ERROR:WS_INVALID_CHANID));
assert(replies==1 && reply_ok==mode);
if(mode==0) unchanged(&ssh,&slot);
free(ssh.modes); cases++;
}
}
int main(void)
{
long page=sysconf(_SC_PAGESIZE); assert(page>0);
byte *p=mmap(NULL,(size_t)page*2,PROT_READ|PROT_WRITE,MAP_PRIVATE|MAP_ANONYMOUS,-1,0);
assert(p!=MAP_FAILED && mprotect(p+page,page,PROT_NONE)==0);
names_matrix(p+page); truncations(p+page); callback_gates(p+page);
assert(munmap(p,(size_t)page*2)==0);
printf("PASS: %u generated channel-name/payload/callback/admission cases\n",cases);
return 0;
}
@@ -0,0 +1,86 @@
"""Exact generated channel handler plus pinned real application callback gates."""
import hashlib
import os
from pathlib import Path
import subprocess
HERE = Path(__file__).resolve().parent
ROOT = HERE.parents[1]
APP_PINS = {
'accept_shell': '0b625bd3e2fe109f62d8b4d97315c81b05cfd7a8b994e6f1079cff9f8ca99e09',
'reject_channel_request': '871a700936602815f072c240c5a0c232dd113f7ad8744367cda15034d4682a49',
'process_handshake': '2ac274e755d553dac4c19aa45240307ad01b1f68d23a5cf2977e4466cdbf5aba',
}
def run_contracts(work, source, extract):
names = ('GetBoolean', 'GetUint32', 'GetSize', 'GetString', 'GetStringAlloc',
'GetStringRef', 'DoChannelRequest')
functions = '\n'.join(extract(source, name) for name in names)
actual = work / 'channel_actual.c'
actual.write_text(functions)
app = (ROOT / 'src/ssh_transport.c').read_text()
bodies = []
for name, sha in APP_PINS.items():
body = extract(app, name)
assert hashlib.sha256(body.encode()).hexdigest() == sha, name
bodies.append(body)
# Registered functions and per-session context must still be the ones tested.
for setter, callback in (('Shell', 'accept_shell'), ('Exec', 'reject_channel_request'),
('Subsys', 'reject_channel_request')):
assert app.count(f'wolfSSH_CTX_SetChannelReq{setter}Cb(context, {callback})') == 1
assert app.count('wolfSSH_SetChannelReqCtx(slot->ssh, slot);') == 1
(work / 'channel_application.c').write_text('\n'.join(bodies))
flags = ['cc', '-std=gnu11', '-O2', '-Wall', '-Wextra', '-Werror',
'-Wno-unused-parameter', '-Wno-unused-function', '-I', str(work),
str(HERE / 'channel_request.c')]
env = {**os.environ, 'CCACHE_DISABLE': '1'}
profiles = (('production-term', ('WOLFSSH_TERM',)),
('no-terminal', ()), ('shell-only', ('WOLFSSH_SHELL',)),
('term-shell', ('WOLFSSH_TERM', 'WOLFSSH_SHELL')),
('all-branches', ('WOLFSSH_TERM', 'WOLFSSH_SHELL', 'WOLFSSH_AGENT')))
for label, macros in profiles:
for small in (False, True):
binary = work / 'channel-contract'
defines = [f'-D{macro}' for macro in macros]
if small:
defines.append('-DWOLFSSH_SMALL_STACK')
subprocess.run([*flags, *defines, '-fsanitize=undefined',
'-fsanitize-undefined-trap-on-error', '-o', str(binary)],
check=True, timeout=30, env=env)
subprocess.run([str(binary)], check=True, timeout=30)
print(f'PASS: {label} channel requests, both stack modes', flush=True)
# Each guard must be behaviorally relevant, including inactive firmware
# branches tested separately, not enabled in the production configuration.
mutations = []
for name in ('env', 'shell', 'exec', 'subsystem', 'pty-req', 'window-change',
'exit-status', 'exit-signal', 'auth-agent-req@openssh.com'):
guard = f'typeSz == sizeof("{name}") - 1 &&\n '
comparison = f'WMEMCMP(type, "{name}", sizeof("{name}") - 1) == 0'
mutations.append((name + ' original prefix', guard + comparison,
f'WSTRNCMP(type, "{name}", typeSz) == 0'))
mutations.append((name + ' length guard', guard, ''))
for label, old, new in mutations:
assert functions.count(old) == 1, label
actual.write_text(functions.replace(old, new))
binary = work / 'channel-mutation'
subprocess.run([*flags, '-DWOLFSSH_TERM', '-DWOLFSSH_SHELL', '-DWOLFSSH_AGENT',
'-o', str(binary)], check=True, timeout=30, env=env)
result = subprocess.run([str(binary)], capture_output=True, timeout=30)
assert result.returncode != 0, f'Undetected channel mutation: {label}'
actual.write_text(functions)
print(f'PASS: {len(mutations)} channel prefix/length-guard mutations rejected', flush=True)
application = '\n'.join(bodies)
for old, new in (
('!slot->shell_requested ||\n ', ''),
('wolfSSH_GetSessionType(slot->ssh) != WOLFSSH_SESSION_SHELL', 'false'),
):
assert application.count(old) == 1
(work / 'channel_application.c').write_text(application.replace(old, new))
binary = work / 'admission-mutation'
subprocess.run([*flags, '-DWOLFSSH_TERM', '-o', str(binary)],
check=True, timeout=30, env=env)
result = subprocess.run([str(binary)], capture_output=True, timeout=30)
assert result.returncode != 0, f'Undetected admission gate mutation: {old}'
(work / 'channel_application.c').write_text(application)
print('PASS: 2 real application shell-admission guard mutations rejected', flush=True)
+10 -5
View File
@@ -20,6 +20,8 @@ typedef uint32_t word32;
#define WS_CRYPTO_FAILED -7
#define WS_ECC_E -8
#define WS_ED25519_E -9
#define WS_INVALID_STATE_E -10
#define WS_CHANOPEN_FAILED -11
#define MSGID_USERAUTH_REQUEST 50
#define MSG_ID_SZ 1
#define BOOLEAN_SZ 1
@@ -141,10 +143,10 @@ static void parsers(byte *end)
assert(DoIgnore(&ssh,p,n,&idx)==(good?0:WS_BUFFER_E));
if (good) assert(idx==4+lengths[j]);
idx=0; ssh.clientState=9;
int service=good && lengths[j]<WOLFSSH_MAX_NAMESZ;
assert(DoServiceRequest(&ssh,p,n,&idx)==(service?0:WS_BUFFER_E));
assert(ssh.clientState==(service?42:9));
assert(idx==(service?4+lengths[j]:0));
int bounded=good && lengths[j]<WOLFSSH_MAX_NAMESZ;
assert(DoServiceRequest(&ssh,p,n,&idx)==
(bounded?WS_INVALID_STATE_E:WS_BUFFER_E));
assert(ssh.clientState==9 && idx==0);
char out[10]; memset(out, 0x55, sizeof(out));
word32 cap=8; idx=0;
assert(GetString(out+1,&cap,p,n,&idx)==(good?0:WS_BUFFER_E));
@@ -169,7 +171,7 @@ static void parsers(byte *end)
byte p[12]={0}; put(p+3,5); word32 idx=3;
assert(DoIgnore(&ssh,p,12,&idx)==0 && idx==12);
put(p+3,0); idx=3;
assert(DoServiceRequest(&ssh,p,7,&idx)==0 && idx==7);
assert(DoServiceRequest(&ssh,p,7,&idx)==WS_INVALID_STATE_E && idx==3);
}
static void windows(byte *end)
{
@@ -243,12 +245,15 @@ static void ecc(byte *end)
}
#include "auth_framing.c"
#include "remaining.c"
int main(void)
{
long page=sysconf(_SC_PAGESIZE); assert(page>0);
byte *map=mmap(NULL,(size_t)page*2,PROT_READ|PROT_WRITE,MAP_PRIVATE|MAP_ANONYMOUS,-1,0);
assert(map!=MAP_FAILED && mprotect(map+page,page,PROT_NONE)==0);
parsers(map+page); windows(map+page); ecc(map+page);
remaining_parsers(map+page);
ecc_framing(map+page); ed25519_framing(map+page);
assert(munmap(map,(size_t)page*2)==0);
printf("PASS: %u parser/window/ECC/Ed25519 cases, guard pages + UBSan trap\n",cases);
+113
View File
@@ -0,0 +1,113 @@
From d2eeec5e263a4821c90805963eeb0666e99868a6 Mon Sep 17 00:00:00 2001
From: Yosuke Shimizu <yosuke@wolfssl.com>
Date: Tue, 24 Mar 2026 11:16:16 +0900
Subject: [PATCH] Fix minor issues
---
src/internal.c | 17 ++++++-----------
src/port.c | 14 ++++++++------
src/wolfterm.c | 2 +-
3 files changed, 15 insertions(+), 18 deletions(-)
diff --git a/src/internal.c b/src/internal.c
index 77f165dbb..8dc13dab6 100644
--- a/src/internal.c
+++ b/src/internal.c
@@ -4880,16 +4880,14 @@ static int ParseRSAPubKey(WOLFSSH *ssh,
byte* n;
word32 nSz;
word32 pubKeyIdx = 0;
- word32 scratch;
ret = wc_InitRsaKey(&sigKeyBlock_ptr->sk.rsa.key, ssh->ctx->heap);
if (ret != 0)
ret = WS_RSA_E;
- if (ret == 0)
- ret = GetUint32(&scratch, pubKey, pubKeySz, &pubKeyIdx);
- /* This is the algo name. */
+ /* Skip the algo name. */
+ if (ret == WS_SUCCESS)
+ ret = GetSkip(pubKey, pubKeySz, &pubKeyIdx);
if (ret == WS_SUCCESS) {
- pubKeyIdx += scratch;
ret = GetUint32(&eSz, pubKey, pubKeySz, &pubKeyIdx);
if (ret == WS_SUCCESS && eSz > pubKeySz - pubKeyIdx)
ret = WS_BUFFER_E;
@@ -4932,7 +4930,6 @@ static int ParseECCPubKey(WOLFSSH *ssh,
const byte* q;
word32 qSz, pubKeyIdx = 0;
int primeId = 0;
- word32 scratch;
ret = wc_ecc_init_ex(&sigKeyBlock_ptr->sk.ecc.key, ssh->ctx->heap,
INVALID_DEVID);
@@ -4958,12 +4955,10 @@ static int ParseECCPubKey(WOLFSSH *ssh,
/* Skip the curve name since we're getting it from the algo. */
if (ret == WS_SUCCESS)
- ret = GetUint32(&scratch, pubKey, pubKeySz, &pubKeyIdx);
+ ret = GetSkip(pubKey, pubKeySz, &pubKeyIdx);
- if (ret == WS_SUCCESS) {
- pubKeyIdx += scratch;
+ if (ret == WS_SUCCESS)
ret = GetStringRef(&qSz, &q, pubKey, pubKeySz, &pubKeyIdx);
- }
if (ret == WS_SUCCESS) {
ret = wc_ecc_import_x963_ex(q, qSz,
@@ -9407,7 +9402,7 @@ static int DoChannelFailure(WOLFSSH* ssh, byte* buf, word32 len, word32* idx)
WLOG(WS_LOG_DEBUG, "Entering DoChannelFailure()");
- if (ssh == NULL || buf == NULL || len != 0 || idx == NULL)
+ if (ssh == NULL || buf == NULL || len == 0 || idx == NULL)
ret = WS_BAD_ARGUMENT;
if (ret == WS_SUCCESS)
diff --git a/src/port.c b/src/port.c
index 37ee3ffc7..79546e890 100644
--- a/src/port.c
+++ b/src/port.c
@@ -267,7 +267,7 @@ void* WS_CreateFileA(const char* fileName, unsigned long desiredAccess,
void* WS_FindFirstFileA(const char* fileName,
char* realFileName, size_t realFileNameSz, int* isDir, void* heap)
{
- HANDLE findHandle = NULL;
+ HANDLE findHandle = INVALID_HANDLE_VALUE;
WIN32_FIND_DATAW findFileData;
wchar_t* unicodeFileName;
size_t unicodeFileNameSz = 0;
@@ -295,12 +295,14 @@ void* WS_FindFirstFileA(const char* fileName,
WFREE(unicodeFileName, heap, PORT_DYNTYPE_STRING);
- error = wcstombs_s(NULL, realFileName, realFileNameSz,
- findFileData.cFileName, realFileNameSz);
+ if (findHandle != INVALID_HANDLE_VALUE) {
+ error = wcstombs_s(NULL, realFileName, realFileNameSz,
+ findFileData.cFileName, realFileNameSz);
- if (isDir != NULL) {
- *isDir =
- (findFileData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0;
+ if (isDir != NULL) {
+ *isDir =
+ (findFileData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0;
+ }
}
return (void*)findHandle;
diff --git a/src/wolfterm.c b/src/wolfterm.c
index 63e69d679..e7abe907e 100644
--- a/src/wolfterm.c
+++ b/src/wolfterm.c
@@ -181,7 +181,7 @@ static void doDisplayAttributes(WOLFSSH* ssh, WOLFSSH_HANDLE handle, word32* arg
break;
case 30: /* set black foreground */
- SetConsoleTextAttribute(handle, (atr & ~(WS_MASK_RBGBG)));
+ SetConsoleTextAttribute(handle, (atr & ~(WS_MASK_RBGFG)));
break;
case 31: /* red foreground */
+55
View File
@@ -0,0 +1,55 @@
From ffa646a4b9d47d5d9d6127db140c433c58b1e276 Mon Sep 17 00:00:00 2001
From: Paul Adelsbach <paul.adelsbach@wolfssl.com>
Date: Tue, 7 Apr 2026 08:51:55 -0700
Subject: [PATCH] Add validation for accept request and reply
---
src/internal.c | 28 ++++++++++++++++++++++++++++
1 file changed, 28 insertions(+)
diff --git a/src/internal.c b/src/internal.c
index 77f165dbb..b9b795582 100644
--- a/src/internal.c
+++ b/src/internal.c
@@ -6539,6 +6539,20 @@ static int DoServiceRequest(WOLFSSH* ssh,
ret = GetString(name, &nameSz, buf, len, idx);
+ /* Requested service must be 'ssh-userauth' */
+ if (ret == WS_SUCCESS) {
+ const char* nameUserAuth = IdToName(ID_SERVICE_USERAUTH);
+ if (nameUserAuth == NULL
+ || nameSz != (word32)XSTRLEN(nameUserAuth)
+ || XMEMCMP(name, nameUserAuth, nameSz) != 0) {
+ WLOG(WS_LOG_DEBUG, "Requested unsupported service: %s", name);
+ /* Terminate session, ignore result of disconnect attempt */
+ (void)SendDisconnect(ssh,
+ WOLFSSH_DISCONNECT_SERVICE_NOT_AVAILABLE);
+ ret = WS_INVALID_STATE_E;
+ }
+ }
+
if (ret == WS_SUCCESS) {
WLOG(WS_LOG_DEBUG, "Requesting service: %s", name);
ssh->clientState = CLIENT_USERAUTH_REQUEST_DONE;
@@ -6557,6 +6571,20 @@ static int DoServiceAccept(WOLFSSH* ssh,
ret = GetString(name, &nameSz, buf, len, idx);
+ /* Accepted service must be 'ssh-userauth' */
+ if (ret == WS_SUCCESS) {
+ const char* nameUserAuth = IdToName(ID_SERVICE_USERAUTH);
+ if (nameUserAuth == NULL
+ || nameSz != (word32)XSTRLEN(nameUserAuth)
+ || XMEMCMP(name, nameUserAuth, nameSz) != 0) {
+ WLOG(WS_LOG_DEBUG, "Accepted unexpected service: %s", name);
+ /* Terminate session, ignore result of disconnect attempt */
+ (void)SendDisconnect(ssh,
+ WOLFSSH_DISCONNECT_SERVICE_NOT_AVAILABLE);
+ ret = WS_INVALID_STATE_E;
+ }
+ }
+
if (ret == WS_SUCCESS) {
WLOG(WS_LOG_DEBUG, "Accepted service: %s", name);
ssh->serverState = SERVER_USERAUTH_REQUEST_DONE;
+307
View File
@@ -0,0 +1,307 @@
From fd82a4bcf55935f0801b14bca6be9c71e32ae914 Mon Sep 17 00:00:00 2001
From: Yosuke Shimizu <yosuke@wolfssl.com>
Date: Wed, 15 Apr 2026 11:33:07 +0900
Subject: [PATCH] Fix DoGlobalRequestFwd and Add the regress tests
---
src/internal.c | 27 +++++-
tests/regress.c | 214 ++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 237 insertions(+), 4 deletions(-)
diff --git a/src/internal.c b/src/internal.c
index 11902b7b4..5db18ddb4 100644
--- a/src/internal.c
+++ b/src/internal.c
@@ -8496,16 +8496,35 @@ static int DoGlobalRequestFwd(WOLFSSH* ssh,
isCancel ? " cancel" : "", bindAddr, bindPort);
}
- if (ret == WS_SUCCESS && wantReply) {
- ret = SendGlobalRequestFwdSuccess(ssh, 1, bindPort);
- }
-
if (ret == WS_SUCCESS) {
if (ssh->ctx->fwdCb) {
ret = ssh->ctx->fwdCb(isCancel ? WOLFSSH_FWD_REMOTE_CLEANUP :
WOLFSSH_FWD_REMOTE_SETUP,
ssh->fwdCbCtx, bindAddr, bindPort);
}
+ else {
+ WLOG(WS_LOG_WARN, "No forwarding callback set, rejecting request. "
+ "Set one with wolfSSH_CTX_SetFwdCb().");
+ ret = WS_UNIMPLEMENTED_E;
+ }
+ }
+
+ if (wantReply) {
+ if (ret == WS_SUCCESS) {
+ if (isCancel) {
+ ret = SendRequestSuccess(ssh, 1);
+ }
+ else {
+ ret = SendGlobalRequestFwdSuccess(ssh, 1, bindPort);
+ }
+ }
+ else {
+ ret = SendRequestSuccess(ssh, 0);
+ }
+ }
+ else if (ret == WS_UNIMPLEMENTED_E) {
+ /* No reply expected; silently reject without terminating connection. */
+ ret = WS_SUCCESS;
}
if (bindAddr != NULL)
diff --git a/tests/regress.c b/tests/regress.c
index 5d069d7fc..bf37202d1 100644
--- a/tests/regress.c
+++ b/tests/regress.c
@@ -231,6 +231,21 @@ static word32 BuildDirectTcpipExtra(const char* host, word32 hostPort,
return idx;
}
+
+static word32 BuildGlobalRequestFwdPacket(const char* bindAddr, word32 bindPort,
+ int isCancel, byte wantReply, byte* out, word32 outSz)
+{
+ byte payload[256];
+ word32 idx = 0;
+ const char* reqName = isCancel ? "cancel-tcpip-forward" : "tcpip-forward";
+
+ idx = AppendString(payload, sizeof(payload), idx, reqName);
+ idx = AppendByte (payload, sizeof(payload), idx, wantReply);
+ idx = AppendString(payload, sizeof(payload), idx, bindAddr);
+ idx = AppendUint32(payload, sizeof(payload), idx, bindPort);
+
+ return WrapPacket(MSGID_GLOBAL_REQUEST, payload, idx, out, outSz);
+}
#endif
/* Simple in-memory transport harness */
@@ -957,6 +972,94 @@ static void AssertChannelOpenFailResponse(const ChannelOpenHarness* harness,
AssertTrue(harness->ssh->channelList == NULL);
}
+#ifdef WOLFSSH_FWD
+static word32 ParsePayloadLen(const byte* packet, word32 packetSz)
+{
+ word32 packetLen;
+ byte padLen;
+
+ AssertNotNull(packet);
+ AssertTrue(packetSz >= 6);
+
+ WMEMCPY(&packetLen, packet, sizeof(packetLen));
+ packetLen = ntohl(packetLen);
+ padLen = packet[4];
+
+ AssertTrue(packetLen >= (word32)padLen + 1);
+ AssertTrue(packetSz >= packetLen + 4);
+
+ return packetLen - padLen - 1;
+}
+
+static const byte* ParseGlobalRequestName(const byte* packet, word32 packetSz,
+ word32* nameSz)
+{
+ word32 packetLen;
+ word32 payloadLen;
+ word32 strSz;
+ const byte* payload;
+
+ AssertNotNull(packet);
+ AssertNotNull(nameSz);
+ AssertTrue(packetSz >= 10);
+
+ WMEMCPY(&packetLen, packet, sizeof(packetLen));
+ packetLen = ntohl(packetLen);
+ AssertTrue(packetSz >= packetLen + 4);
+
+ payloadLen = ParsePayloadLen(packet, packetSz);
+ payload = packet + 5;
+
+ AssertTrue(payloadLen >= 1 + sizeof(word32));
+ AssertIntEQ(payload[0], MSGID_GLOBAL_REQUEST);
+
+ WMEMCPY(&strSz, payload + 1, sizeof(strSz));
+ strSz = ntohl(strSz);
+ AssertTrue(payloadLen >= 1 + sizeof(word32) + strSz);
+
+ *nameSz = strSz;
+ return payload + 1 + sizeof(word32);
+}
+
+static void AssertGlobalRequestReply(const ChannelOpenHarness* harness,
+ byte expectedMsgId)
+{
+ byte msgId;
+ word32 payloadLen;
+
+ AssertTrue(harness->io.outSz > 0);
+ msgId = ParseMsgId(harness->io.out, harness->io.outSz);
+ AssertIntEQ(msgId, expectedMsgId);
+
+ payloadLen = ParsePayloadLen(harness->io.out, harness->io.outSz);
+ if (expectedMsgId == MSGID_REQUEST_FAILURE) {
+ AssertIntEQ(payloadLen, 1);
+ }
+ else if (expectedMsgId == MSGID_REQUEST_SUCCESS) {
+ const byte* reqName;
+ word32 reqNameSz;
+
+ reqName = ParseGlobalRequestName(harness->io.in, harness->io.inSz,
+ &reqNameSz);
+
+ if (reqNameSz == sizeof("tcpip-forward") - 1 &&
+ WMEMCMP(reqName, "tcpip-forward",
+ sizeof("tcpip-forward") - 1) == 0) {
+ AssertIntEQ(payloadLen, 5);
+ }
+ else if (reqNameSz == sizeof("cancel-tcpip-forward") - 1 &&
+ WMEMCMP(reqName, "cancel-tcpip-forward",
+ sizeof("cancel-tcpip-forward") - 1) == 0) {
+ AssertIntEQ(payloadLen, 1);
+ }
+ else {
+ Fail(("unexpected global request name"),
+ ("%.*s", (int)reqNameSz, reqName));
+ }
+ }
+}
+#endif
+
static int RejectChannelOpenCb(WOLFSSH_CHANNEL* channel, void* ctx)
{
(void)channel;
@@ -978,6 +1081,17 @@ static int RejectDirectTcpipSetup(WS_FwdCbAction action, void* ctx,
return WS_SUCCESS;
}
+
+static int AcceptFwdCb(WS_FwdCbAction action, void* ctx,
+ const char* host, word32 port)
+{
+ (void)action;
+ (void)ctx;
+ (void)host;
+ (void)port;
+
+ return WS_SUCCESS;
+}
#endif
@@ -1242,6 +1356,101 @@ static void TestDirectTcpipNoFwdCbSendsOpenFail(void)
FreeChannelOpenHarness(&harness);
}
+
+static void TestGlobalRequestFwdNoCbSendsFailure(void)
+{
+ ChannelOpenHarness harness;
+ byte in[256];
+ word32 inSz;
+ int ret;
+
+ inSz = BuildGlobalRequestFwdPacket("0.0.0.0", 2222, 0, 1, in, sizeof(in));
+ InitChannelOpenHarness(&harness, in, inSz);
+ /* no fwdCb registered */
+
+ ret = DoReceive(harness.ssh);
+
+ AssertIntEQ(ret, WS_SUCCESS);
+ AssertGlobalRequestReply(&harness, MSGID_REQUEST_FAILURE);
+
+ FreeChannelOpenHarness(&harness);
+}
+
+static void TestGlobalRequestFwdNoCbNoReplyKeepsConnection(void)
+{
+ ChannelOpenHarness harness;
+ byte in[256];
+ word32 inSz;
+ int ret;
+
+ /* wantReply=0: no reply sent, connection must stay alive */
+ inSz = BuildGlobalRequestFwdPacket("0.0.0.0", 2222, 0, 0, in, sizeof(in));
+ InitChannelOpenHarness(&harness, in, inSz);
+ /* no fwdCb registered */
+
+ ret = DoReceive(harness.ssh);
+
+ AssertIntEQ(ret, WS_SUCCESS);
+ AssertIntEQ(harness.io.outSz, 0); /* no reply sent */
+
+ FreeChannelOpenHarness(&harness);
+}
+
+static void TestGlobalRequestFwdWithCbSendsSuccess(void)
+{
+ ChannelOpenHarness harness;
+ byte in[256];
+ word32 inSz;
+ int ret;
+
+ inSz = BuildGlobalRequestFwdPacket("0.0.0.0", 2222, 0, 1, in, sizeof(in));
+ InitChannelOpenHarness(&harness, in, inSz);
+ AssertIntEQ(wolfSSH_CTX_SetFwdCb(harness.ctx, AcceptFwdCb, NULL), WS_SUCCESS);
+
+ ret = DoReceive(harness.ssh);
+
+ AssertIntEQ(ret, WS_SUCCESS);
+ AssertGlobalRequestReply(&harness, MSGID_REQUEST_SUCCESS);
+
+ FreeChannelOpenHarness(&harness);
+}
+
+static void TestGlobalRequestFwdCancelNoCbSendsFailure(void)
+{
+ ChannelOpenHarness harness;
+ byte in[256];
+ word32 inSz;
+ int ret;
+
+ inSz = BuildGlobalRequestFwdPacket("0.0.0.0", 2222, 1, 1, in, sizeof(in));
+ InitChannelOpenHarness(&harness, in, inSz);
+
+ ret = DoReceive(harness.ssh);
+
+ AssertIntEQ(ret, WS_SUCCESS);
+ AssertGlobalRequestReply(&harness, MSGID_REQUEST_FAILURE);
+
+ FreeChannelOpenHarness(&harness);
+}
+
+static void TestGlobalRequestFwdCancelWithCbSendsSuccess(void)
+{
+ ChannelOpenHarness harness;
+ byte in[256];
+ word32 inSz;
+ int ret;
+
+ inSz = BuildGlobalRequestFwdPacket("0.0.0.0", 2222, 1, 1, in, sizeof(in));
+ InitChannelOpenHarness(&harness, in, inSz);
+ AssertIntEQ(wolfSSH_CTX_SetFwdCb(harness.ctx, AcceptFwdCb, NULL), WS_SUCCESS);
+
+ ret = DoReceive(harness.ssh);
+
+ AssertIntEQ(ret, WS_SUCCESS);
+ AssertGlobalRequestReply(&harness, MSGID_REQUEST_SUCCESS);
+
+ FreeChannelOpenHarness(&harness);
+}
#endif
#ifdef WOLFSSH_AGENT
@@ -1707,6 +1916,11 @@ int main(int argc, char** argv)
#ifdef WOLFSSH_FWD
TestDirectTcpipRejectSendsOpenFail();
TestDirectTcpipNoFwdCbSendsOpenFail();
+ TestGlobalRequestFwdNoCbSendsFailure();
+ TestGlobalRequestFwdNoCbNoReplyKeepsConnection();
+ TestGlobalRequestFwdWithCbSendsSuccess();
+ TestGlobalRequestFwdCancelNoCbSendsFailure();
+ TestGlobalRequestFwdCancelWithCbSendsSuccess();
#endif
#ifdef WOLFSSH_AGENT
TestAgentChannelNullAgentSendsOpenFail();
+69
View File
@@ -0,0 +1,69 @@
From 0317c40fc131fab952d291d43c56c7b7ce5f4303 Mon Sep 17 00:00:00 2001
From: Yosuke Shimizu <yosuke@wolfssl.com>
Date: Wed, 15 Apr 2026 13:37:21 +0900
Subject: [PATCH] Fix DoChannelOpen() and Add regress test
---
src/internal.c | 6 ++++++
tests/regress.c | 24 ++++++++++++++++++++++++
2 files changed, 30 insertions(+)
diff --git a/src/internal.c b/src/internal.c
index 1202d132e..a0df29600 100644
--- a/src/internal.c
+++ b/src/internal.c
@@ -8739,6 +8739,12 @@ static int DoChannelOpen(WOLFSSH* ssh,
ssh->fwdCbCtx, NULL, newChannel->channel);
}
}
+ else {
+ WLOG(WS_LOG_WARN, "No forward callback set for direct-tcpip channel,"
+ " failing channel open");
+ fail_reason = OPEN_ADMINISTRATIVELY_PROHIBITED;
+ ret = WS_ERROR;
+ }
}
#endif /* WOLFSSH_FWD */
if (ret == WS_SUCCESS) {
diff --git a/tests/regress.c b/tests/regress.c
index 321151b8d..bfc719315 100644
--- a/tests/regress.c
+++ b/tests/regress.c
@@ -1184,6 +1184,29 @@ static void TestDirectTcpipRejectSendsOpenFail(void)
FreeChannelOpenHarness(&harness);
}
+
+static void TestDirectTcpipNoFwdCbSendsOpenFail(void)
+{
+ ChannelOpenHarness harness;
+ byte extra[128];
+ byte in[192];
+ word32 extraSz;
+ word32 inSz;
+ int ret;
+
+ extraSz = BuildDirectTcpipExtra("127.0.0.1", 8080, "127.0.0.1", 2222,
+ extra, sizeof(extra));
+ inSz = BuildChannelOpenPacket("direct-tcpip", 9, 0x4000, 0x8000,
+ extra, extraSz, in, sizeof(in));
+
+ InitChannelOpenHarness(&harness, in, inSz);
+ /* Intentionally do NOT register fwdCb */
+
+ ret = DoReceive(harness.ssh);
+ AssertChannelOpenFailResponse(&harness, ret);
+
+ FreeChannelOpenHarness(&harness);
+}
#endif
#ifdef WOLFSSH_AGENT
@@ -1648,6 +1671,7 @@ int main(int argc, char** argv)
TestChannelOpenCallbackRejectSendsOpenFail();
#ifdef WOLFSSH_FWD
TestDirectTcpipRejectSendsOpenFail();
+ TestDirectTcpipNoFwdCbSendsOpenFail();
#endif
#ifdef WOLFSSH_AGENT
TestAgentChannelNullAgentSendsOpenFail();
@@ -0,0 +1,26 @@
{
"899": {
"url": "https://patch-diff.githubusercontent.com/raw/wolfSSL/wolfssh/pull/899.patch",
"commit_url": "https://github.com/wolfSSL/wolfssh/commit/d2eeec5e263a4821c90805963eeb0666e99868a6.patch",
"commit": "d2eeec5e263a4821c90805963eeb0666e99868a6",
"sha256": "e33c8b0aaa3c5a5d6c5201e6141cafd2383a1ed6931fea6655a9b757fbe0b0ce"
},
"902": {
"url": "https://patch-diff.githubusercontent.com/raw/wolfSSL/wolfssh/pull/902.patch",
"commit_url": "https://github.com/wolfSSL/wolfssh/commit/ffa646a4b9d47d5d9d6127db140c433c58b1e276.patch",
"commit": "ffa646a4b9d47d5d9d6127db140c433c58b1e276",
"sha256": "4dc3a69f8cecb34f5091b9c22e0012ea6020b7ede9c1ab3de4d96264168c345a"
},
"918": {
"url": "https://patch-diff.githubusercontent.com/raw/wolfSSL/wolfssh/pull/918.patch",
"commit_url": "https://github.com/wolfSSL/wolfssh/commit/fd82a4bcf55935f0801b14bca6be9c71e32ae914.patch",
"commit": "fd82a4bcf55935f0801b14bca6be9c71e32ae914",
"sha256": "a2a59707086c6273a2c339e63db49846a5a1862c73924b60a845a03d7d41be41"
},
"919": {
"url": "https://patch-diff.githubusercontent.com/raw/wolfSSL/wolfssh/pull/919.patch",
"commit_url": "https://github.com/wolfSSL/wolfssh/commit/0317c40fc131fab952d291d43c56c7b7ce5f4303.patch",
"commit": "0317c40fc131fab952d291d43c56c7b7ce5f4303",
"sha256": "753fffea9925deeed4ddfc7aeba1fce0e950602b52ed1434bd817c2d48857b2f"
}
}
+65
View File
@@ -0,0 +1,65 @@
/* SPDX-License-Identifier: GPL-3.0-only */
static void remaining_parsers(byte *end)
{
WOLFSSH ssh = {.clientState=9};
const byte service[] = "ssh-userauth";
for (word32 offset=0; offset<=3; offset++) {
for (word32 size=0; size<=16; size++) {
byte *p=end-offset-size;
memset(p,0,offset+size);
if (size>=4) {
put(p+offset,12);
memcpy(p+offset+4,service,size-4);
}
word32 idx=offset;
ssh.clientState=9;
assert(DoServiceRequest(&ssh,p,offset+size,&idx)==
(size==16?WS_SUCCESS:WS_BUFFER_E));
assert(idx==(size==16?offset+size:offset));
assert(ssh.clientState==(size==16?42:9));
cases++;
}
}
/* Equal-length mismatch at every byte, embedded NUL, prefix and suffix. */
for (word32 n=0;n<=13;n++) {
byte *p=end-4-n;
put(p,n); memcpy(p+4,service,n);
word32 idx=0; ssh.clientState=9;
assert(DoServiceRequest(&ssh,p,n+4,&idx)==
(n==12?WS_SUCCESS:WS_INVALID_STATE_E));
assert(idx==(n==12?n+4:0) && ssh.clientState==(n==12?42:9));
cases++;
}
for (word32 pos=0;pos<12;pos++) {
byte *p=end-16; put(p,12); memcpy(p+4,service,12); p[4+pos]=0;
word32 idx=0; ssh.clientState=9;
assert(DoServiceRequest(&ssh,p,16,&idx)==WS_INVALID_STATE_E);
assert(idx==0 && ssh.clientState==9); cases++;
}
/* CHANNEL_FAILURE has exactly one recipient; never mutate session/channel. */
for (word32 offset=0;offset<=3;offset++) {
for (word32 size=0;size<=8;size++) {
byte *p=end-offset-size; memset(p,0,offset+size);
if(size>=4) put(p+offset,7);
word32 idx=offset; finds=0; channel.peerWindowSz=123;
ssh.clientState=9;
assert(DoChannelFailure(&ssh,p,offset+size,&idx)==
(size==4?WS_CHANOPEN_FAILED:WS_BUFFER_E));
assert(idx==(size==4?offset+4:offset));
assert(finds==(size==4?1u:0u));
assert(ssh.clientState==9 && channel.peerWindowSz==123); cases++;
}
}
byte *p=end-4; put(p,8); word32 idx=0;
assert(DoChannelFailure(&ssh,p,4,&idx)==WS_INVALID_CHANID && idx==0);
const word32 invalid[]={4,5,UINT32_MAX-3,UINT32_MAX};
for(unsigned j=0;j<4;j++) {
idx=invalid[j]; finds=0;
assert(DoChannelFailure(&ssh,p,4,&idx)==WS_BUFFER_E);
assert(idx==invalid[j] && finds==0); cases++;
}
idx=0;
assert(DoChannelFailure(NULL,p,4,&idx)==WS_BAD_ARGUMENT);
assert(DoChannelFailure(&ssh,NULL,4,&idx)==WS_BAD_ARGUMENT);
assert(DoChannelFailure(&ssh,p,4,NULL)==WS_BAD_ARGUMENT);
}
+181
View File
@@ -0,0 +1,181 @@
#!/usr/bin/env python3
"""Finite remaining-parser source/provenance contract; optional read-only profile replay."""
import hashlib
import json
import os
from pathlib import Path
import re
import shlex
import subprocess
import sys
sys.dont_write_bytecode = True
ROOT = Path(__file__).resolve().parents[2]
HERE = Path(__file__).resolve().parent
sys.path.insert(0, str(ROOT / 'tests/wolfssh_auth_contract'))
from run import extract
from security_overrides import ENTRIES, render_entry
# Independently specified delta against the reviewed original+ordering+parser
# baseline. Reversing precisely these bytes must recover its whole-source hash.
BASELINE_SHA = '4948f8c447670eb54153dd1f3db69e4fa3092f7d7f7ed58a18a8fa05fcd168ca'
NOTICE = '''/* Server parser review modified 2026-09-16: bounded CHANNEL_FAILURE
* and ssh-userauth service validation; PR899/902 subset, not full PRs.
* Local follow-up: exact bounded channel-request names.
* Provenance/limits: docs/ssh_parser_remaining_review.md.
*/
'''
SERVICE = ''' /* PR902 current-server subset: reject before publishing the transition.
* The owner closes on this error; no best-effort disconnect is queued. */
if (nameSz != sizeof("ssh-userauth") - 1 ||
WMEMCMP(serviceName, "ssh-userauth", sizeof("ssh-userauth") - 1) != 0)
return WS_INVALID_STATE_E;
'''
OLD_FAILURE = ''' if (ssh == NULL || buf == NULL || len != 0 || idx == NULL)
ret = WS_BAD_ARGUMENT;
if (ret == WS_SUCCESS)
ret = WS_CHANOPEN_FAILED;'''
NEW_FAILURE = ''' word32 begin, channelId;
if (ssh == NULL || buf == NULL || idx == NULL)
return WS_BAD_ARGUMENT;
begin = *idx;
ret = GetUint32(&channelId, buf, len, &begin);
if (ret != WS_SUCCESS)
return ret;
if (begin != len)
return WS_BUFFER_E;
if (ChannelFind(ssh, channelId, WS_CHANNEL_ID_SELF) == NULL)
return WS_INVALID_CHANID;
*idx = begin;
ret = WS_CHANOPEN_FAILED;'''
PINS = {
'tools/wolfssh_order/delta.json': '6a81376fe3ffc5f449cde105402963f52d2d78cc844153e869a7e1e0f734fb76',
'managed_components/wolfssl__wolfssl/wolfcrypt/src/signature.c': '62ab3db3dfd251b2a2c73b69ef05aab6085d2e0d673fd9159514b3ee261cea4f',
}
PATCHES = {
'899': ('d2eeec5e263a4821c90805963eeb0666e99868a6', 'e33c8b0aaa3c5a5d6c5201e6141cafd2383a1ed6931fea6655a9b757fbe0b0ce'),
'902': ('ffa646a4b9d47d5d9d6127db140c433c58b1e276', '4dc3a69f8cecb34f5091b9c22e0012ea6020b7ede9c1ab3de4d96264168c345a'),
'918': ('fd82a4bcf55935f0801b14bca6be9c71e32ae914', 'a2a59707086c6273a2c339e63db49846a5a1862c73924b60a845a03d7d41be41'),
'919': ('0317c40fc131fab952d291d43c56c7b7ce5f4303', '753fffea9925deeed4ddfc7aeba1fce0e950602b52ed1434bd817c2d48857b2f'),
}
def digest(data):
return hashlib.sha256(data).hexdigest()
def check_sources(original, generated):
assert digest(original.read_bytes()) == \
'81ff1f9166708abd5c2911e9fe57c0aee01c88b5d3f68c909ee8a856d37f36a9'
text = generated.decode()
prior = text
for new, old in ((NOTICE, ''), (SERVICE, ''), (NEW_FAILURE, OLD_FAILURE)):
assert prior.count(new) == 1, 'Independent delta anchor changed'
prior = prior.replace(new, old)
# Independent name list/format, never imported from the generator's edits.
for name in ('env', 'shell', 'exec', 'subsystem', 'pty-req', 'window-change',
'exit-status', 'exit-signal', 'auth-agent-req@openssh.com'):
new = (f'typeSz == sizeof("{name}") - 1 &&\n'
f' WMEMCMP(type, "{name}", sizeof("{name}") - 1) == 0')
old = f'WSTRNCMP(type, "{name}", typeSz) == 0'
assert prior.count(new) == 1, name
prior = prior.replace(new, old)
assert digest(prior.encode()) == BASELINE_SHA, 'Unreviewed generated-source delta'
assert extract(prior, 'DoChannelRequest') == extract(original.read_text(), 'DoChannelRequest')
for path, sha in PINS.items():
assert digest((ROOT / path).read_bytes()) == sha, path
provenance = json.loads((HERE / 'provenance.json').read_text())
assert set(provenance) == set(PATCHES)
for number, (commit, sha) in PATCHES.items():
data = (HERE / f'pr{number}.patch').read_bytes()
assert digest(data) == sha and data.startswith(f'From {commit} '.encode())
assert provenance[number] == {
'url': f'https://patch-diff.githubusercontent.com/raw/wolfSSL/wolfssh/pull/{number}.patch',
'commit_url': f'https://github.com/wolfSSL/wolfssh/commit/{commit}.patch',
'commit': commit, 'sha256': sha,
}
for name in ('ParseRSAPubKey', 'ParseECCPubKey', 'ParsePubKey', 'DoKexDhReply',
'DoChannelOpen', 'DoGlobalRequest',
'DoServiceAccept'):
# DoKexDhReply has an ordering delta, checked by the whole baseline pin.
if name != 'DoKexDhReply':
assert extract(text, name) == extract(original.read_text(), name), name
# SignHEcdsa has preprocessor-selected bodies after its signature; compare
# its complete region rather than using the single-body extractor.
start, end = 'static int SignHEcdsa(', 'static int SignH('
def signer_region(source):
begin = source.index(start)
return source[begin:source.index(end, begin)]
assert signer_region(text) == signer_region(original.read_text())
assert len(re.findall(r'\bParsePubKey\s*\(', text)) == 2
assert 'ParsePubKey(ssh, sigKeyBlock_ptr, pubKey, pubKeySz)' in extract(text, 'DoKexDhReply')
assert len(re.findall(r'\bParseECCPubKey\s*\(', text)) == 2
assert len(re.findall(r'\bParseRSAPubKey\s*\(', text)) == 2
packet = extract(text, 'DoPacket')
assert packet.index('IsMessageAllowed(ssh, msg, WS_MSG_RECV)') < packet.index('switch (msg)')
print('PASS: independent whole-source delta, unchanged ordering/client/request branch bodies, archived PR provenance')
return prior.encode()
def check_profile(generated, prior):
databases = list((ROOT / '.pio/build').glob('*/compile_commands.json'))
assert len(databases) == 1, 'Need one unambiguous production compile database'
entries = json.loads(databases[0].read_text())
matches = []
for entry in entries:
path = (Path(entry['directory']) / entry['file']).resolve()
assert path != ROOT / 'managed_components/wolfssl__wolfssh/src/internal.c'
if path.parts[-3:] == ('security_overrides', 'wolfssh_internal', 'internal.c'):
matches.append((entry, path))
assert len(matches) == 1
entry, path = matches[0]
actual = path.read_bytes()
assert actual in (generated, prior), 'Unreviewed production generated input'
args = entry.get('arguments') or shlex.split(entry['command'])
clean = []; skip = False
for arg in args:
if skip:
skip = False
elif arg in ('-o', '-MF', '-MT', '-MQ'):
skip = True
elif arg not in ('-c', '-MD', '-MMD', '-MP'):
clean.append(arg)
result = subprocess.run(clean + ['-E', '-dM'], cwd=entry['directory'],
capture_output=True, text=True, check=True, timeout=60,
env={**os.environ, 'CCACHE_DISABLE': '1'})
macros = dict(re.findall(r'^#define (\w+)(?: (.*))?$', result.stdout, re.M))
assert macros.get('LIBWOLFSSH_VERSION_HEX') == '0x01004020'
for name in ('WOLFSSH_FWD', 'WOLFSSH_AGENT', 'WOLFSSH_CERTS', 'WOLFSSH_SFTP',
'WOLFSSH_SCP', 'WOLFSSH_NO_ECDSA', 'WOLFSSH_NO_ED25519',
'NO_WOLFSSH_SERVER', 'NO_WOLFSSH_CLIENT', 'WOLFSSH_SHELL'):
assert name not in macros, name
for name in ('WOLFSSH_NO_RSA', 'WOLFSSH_NO_DH', 'WOLFSSL_VALIDATE_ECC_IMPORT',
'WOLFSSL_ECDHX_SHARED_NOT_ZERO', 'CURVE25519_SMALL', 'ED25519_SMALL',
'WOLFSSH_TERM'):
assert name in macros, name
# Compile the fresh source with the saved real target flags, without changing
# the production build tree or creating objects/dependency files.
import tempfile
with tempfile.TemporaryDirectory(prefix='ssh-parser-syntax-') as directory:
fresh = Path(directory) / 'internal.c'; fresh.write_bytes(generated)
syntax = [str(fresh) if (Path(entry['directory']) / arg).resolve() == path
else arg for arg in clean]
subprocess.run(syntax + ['-fsyntax-only'], cwd=entry['directory'], check=True,
timeout=60, env={**os.environ, 'CCACHE_DISABLE': '1'})
print('PASS: actual Xtensa profile + fresh generated source syntax (no objects/build regeneration)')
print('Production generated source:', 'current' if actual == generated else
'REVIEWED PRIOR BASELINE; regeneration/build still required')
if __name__ == '__main__':
assert sys.argv[1:] in ([], ['--profile'])
entry = next(e for e in ENTRIES if e.name == 'wolfssh_internal')
original, generated = render_entry(entry, {'project': ROOT})
prior = check_sources(original, generated)
print('Fresh generated SHA-256:', digest(generated))
if '--profile' in sys.argv:
check_profile(generated, prior)
+18 -2
View File
@@ -18,15 +18,17 @@ entry = next(e for e in ENTRIES if e.name == 'wolfssh_internal')
assert entry.sha256 == '81ff1f9166708abd5c2911e9fe57c0aee01c88b5d3f68c909ee8a856d37f36a9'
original, generated = render_entry(entry, {'project': ROOT})
names = ('GetUint32', 'GetSize', 'GetString', 'GetSkip', 'GetStringRef',
'DoIgnore', 'DoServiceRequest', 'DoChannelWindowAdjust', 'DoUserAuthRequestEcc',
'DoIgnore', 'DoServiceRequest', 'DoChannelFailure', 'DoChannelWindowAdjust', 'DoUserAuthRequestEcc',
'DoUserAuthRequestEd25519')
# 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(), tuple(
edit for edit in entry.edits if edit not in WOLFSSH_PARSER_EDITS))
for name in ('DoUserAuthRequestPassword', 'DoPacket', 'DoChannelFailure',
for name in ('DoUserAuthRequestPassword', 'DoPacket',
'ParseRSAPubKey', 'ParseECCPubKey', 'DoUserAuthRequestPublicKey'):
assert extract(generated.decode(), name) == extract(baseline, name), name
from review import check_sources
check_sources(original, generated)
with tempfile.TemporaryDirectory(prefix='wolfssh-parser-') as directory:
work = Path(directory)
# Read back the actual generated bytes, not a parallel implementation.
@@ -46,6 +48,18 @@ with tempfile.TemporaryDirectory(prefix='wolfssh-parser-') as directory:
# Prove negative fixtures detect removal of each new boundary/type guard.
# Mutations affect only temporary extracted host copies, never the override.
mutations = (
('Service exact length', 'DoServiceRequest',
(('nameSz != sizeof("ssh-userauth") - 1 ||', '0 ||'),)),
('Service exact bytes', 'DoServiceRequest',
(('WMEMCMP(serviceName, "ssh-userauth", sizeof("ssh-userauth") - 1) != 0', '0'),)),
('Failure bounded recipient', 'DoChannelFailure',
(('ret = GetUint32(&channelId, buf, len, &begin);',
'ato32(buf + begin, &channelId); begin += 4; ret = WS_SUCCESS;'),)),
('Failure exact consumption', 'DoChannelFailure',
(('if (begin != len)', 'if (0)'),)),
('Failure known recipient', 'DoChannelFailure',
(('if (ChannelFind(ssh, channelId, WS_CHANNEL_ID_SELF) == NULL)',
'if (0 && ChannelFind(ssh, channelId, WS_CHANNEL_ID_SELF) == NULL)'),)),
('ECC nested read boundary', 'DoUserAuthRequestEcc',
(('pk->signature, sz, &i)', 'pk->signature, pk->signatureSz, &i)'),)),
('ECC inner exact consumption', 'DoUserAuthRequestEcc',
@@ -76,5 +90,7 @@ 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')
from channel_request import run_contracts
run_contracts(work, source.read_text(), extract)
print('PASS: exact original hash; generated parser; parser-isolated ordering/password/deferred functions')
print('NOTE: production build-tree registration/firmware not regenerated or validated')
+109
View File
@@ -0,0 +1,109 @@
# Scoped release notice inputs and unresolved obligations
Reviewed local notice snapshot: **2026-09-16**. This is engineering evidence,
not legal advice, legal clearance, a complete SBOM, or corresponding source.
The bundle preserves separate files, not a single inferred umbrella license.
## Provenance and scope
`inputs.json` is the explicit, reviewed input policy for
`tools/release_notices.py`. It records logical root/path, full-file size and
SHA-256, optional zero-based half-open byte range, output SHA-256, and purpose.
A null range means the **entire original file** is retained. Ranges select
opening source notice comments verbatim, not source implementations. Even
excerpts require the exact hash of the whole installed source file. Absolute
machine paths, collection time, build artifacts and configuration are omitted.
The manifest in each successful bundle repeats this evidence and identifies
the catalog by hash. The catalog is trusted project policy, not a signed SBOM.
Roots identify the project checkout, installed ESP-IDF package, and installed
Xtensa toolchain. Managed manifests and package metadata are retained as
provenance, not as substitutes for licenses. Hashes identify the reviewed local
bytes; they do not authenticate upstream authorship or validate all package
contents. Versions: ESP-IDF 5.5.0 / package 3.50500.0, Xtensa package
14.2.0+20241119, esp_tinyusb 2.2.1, led_strip 3.0.3, mDNS 1.12.0,
TinyUSB 0.21.0~1, wolfSSL 5.8.2~1, wolfSSH 1.4.20.
The scoped inventory covers the six managed packages, the SDK/runtime groups
in `docs/dependency_licenses.md`, two xterm packages, and the recorded icon
summary. It retains configured-but-not-observed-linked groups too; inclusion
is not a claim that each contributes to a release image. Full SDK/toolchain
redistribution needs a broader review, including nested/test/tool licenses.
No recursive license discovery is performed by the tool. Newly added files or
NOTICEs outside the explicit list are not detected: review the catalog again
when dependencies, selected objects, or distribution scope change.
## Grants and evidence that must not be collapsed
- Project: GPL-3.0-only; dependencies retain their own grants.
- wolfSSL: preserve package LICENSE and README; GPLv3 option selected here.
- **wolfSSH packaging discrepancy remains unresolved:** installed LICENSE and
README describe wolfSSL/wolfCrypt and GPLv2-or-later; wolfSSH source headers
explicitly grant GPL-3.0-or-later. Both evidence sets are included unchanged.
GPLv3 is selected for this integration, not GPL-2.0-only. Seek upstream
packaging clarification; this bundle does not silently repair vendor files.
- Mbed TLS: preserve the whole dual-license document (Apache-2.0 OR
GPL-2.0-or-later); Apache-2.0 is the selection for this integration.
- Preserve **all** argtable3 aggregate notices and **both** SDK and toolchain
Newlib texts. Neither aggregate is reduced to one guessed SPDX label.
- GCC: preserve GPLv3 and the actual GCC Runtime Library Exception 3.1;
COPYING3.LIB is not a substitute. Eligibility/per-object review remains.
- wpa_supplicant: retain COPYING **and** README containing the full BSD grant;
COPYING's historical GPL option discussion is not a current GPLv2-only grant.
- Xtensa HAL and FatFs: retain complete opening permission/disclaimer comments.
- TinyUSB's nested Espressif LED-strip license is retained separately; it is
not a seventh managed package.
## TLSF full-text assembly
`TLSF-BSD-3-Clause.txt` is a **project-assembled rendering**, not a claimed
verbatim upstream LICENSE. Copyright holder and years come from the installed
IDF 5.5.0 files `components/heap/tlsf/include/tlsf.h` and
`components/heap/tlsf/tlsf_block_functions.h`: both identify **20062016 Matthew
Conte** and **BSD-3-Clause**. The three conditions and disclaimer render that
SPDX license, with this holder, rather than another project's copyright.
The original SPDX comments are retained as separately hash-pinned excerpts;
`components/heap/tlsf/README.md` also records the 2016 switch to BSD. No new
license grant or independent upstream authentication is claimed by assembly.
## Icons and browser notices
The existing Pictogrammers summary is retained unchanged. Full Apache-2.0 text
is supplied as `inputs/sdk/LICENSE`, the exact installed SDK copy; this is
license text, **not proof of the individual icons' provenance or grant**.
The project records Material Design Icons 7.4.47 and adapted USB/Wi-Fi masks.
Exact two-icon origin/version/licensing and any applicable upstream NOTICE
still need verification before distribution. No icon or web asset was changed
or regenerated, and no upstream icon provenance was authenticated here.
Both xterm MIT notices and `web_assets/SOURCES.md` are retained. This does not
embed or serve notices in browser responses, authenticate preferred sources,
or independently establish project-logo authorship. Arrange and verify actual
firmware/device/browser recipient notice delivery; Git presence alone is not
proof of delivery.
## Remaining release gates — bundle success does not close these
1. **Radio blobs:** establish required corresponding source or a defensible
System Library/other exception for actual linked content. The three local
Apache radio license files permit redistribution on their terms; they do
not settle GPL corresponding-source compatibility. No clearance or blanket
prohibition is asserted.
2. Review final application **and bootloader** maps, runtime objects, ROM,
mixed/aggregate and nested package attributions. The catalog is bounded,
not an exhaustive selected-object audit or all-upstream-NOTICE inventory.
3. Deliver exact preferred corresponding source through an appropriate GPLv3
method. Include application and required dependency sources, safe build
inputs, editable generators/patches, original hash-matching overlay inputs,
`tools/security_overrides.py`, `cmake/security_overrides.cmake`, root CMake
integration, `cmake/wolf_crypto_policy.{cmake,h}`, and **all**
`tools/wolfssh_order/` delta/patch/provenance inputs. Retain upstream grants
and generated modification/date notices (2026-09-15 baseline and
2026-09-16 wolfSSH ordering). There are eight generated C sources and one
PUBLIC forced header. This notice tool does **not** package those sources.
4. Supply required asset preferred sources/build inputs, assess User Product
Installation Information, and validate recipient access and source offers.
Do not publish keys, passwords, Wi-Fi secrets, tickets, verifiers, NVS/flash
images, credential backups, or secret-bearing build configurations.
5. Resolve wolfSSH packaging and icon provenance questions above. Do not use a
successful hash check as legal sign-off or whole-Phase-9 acceptance.
+28
View File
@@ -0,0 +1,28 @@
Copyright (c) 2006-2016 Matthew Conte
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its contributors
may be used to endorse or promote products derived from this software
without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.
+619
View File
@@ -0,0 +1,619 @@
{
"inputs": [
{
"output_sha256": "8ceb4b9ee5adedde47b31e975c1d90c73ad27b6b165a1dcd80c7c545eb65b903",
"path": "LICENSE",
"purpose": "Project GPL-3.0-only full text",
"range": null,
"root": "project",
"sha256": "8ceb4b9ee5adedde47b31e975c1d90c73ad27b6b165a1dcd80c7c545eb65b903",
"size": 35147
},
{
"output_sha256": "2d1c62093cafdb54cd209813c526c35e2e470b62cc6f5b0cde9a040569662726",
"path": "dependencies.lock",
"purpose": "Exact resolved package identity; not complete package-content verification",
"range": null,
"root": "project",
"sha256": "2d1c62093cafdb54cd209813c526c35e2e470b62cc6f5b0cde9a040569662726",
"size": 2239
},
{
"output_sha256": "cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30",
"path": "managed_components/espressif__esp_tinyusb/LICENSE",
"purpose": "espressif__esp_tinyusb 2.2.1 complete package license; preserve original grant",
"range": null,
"root": "project",
"sha256": "cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30",
"size": 11358
},
{
"output_sha256": "425288a2ceb030d3890a11e741408737769210f6c94b86e5d8af934969c1098e",
"path": "managed_components/espressif__esp_tinyusb/idf_component.yml",
"purpose": "Installed espressif__esp_tinyusb 2.2.1 manifest/provenance",
"range": null,
"root": "project",
"sha256": "425288a2ceb030d3890a11e741408737769210f6c94b86e5d8af934969c1098e",
"size": 552
},
{
"output_sha256": "cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30",
"path": "managed_components/espressif__led_strip/LICENSE",
"purpose": "espressif__led_strip 3.0.3 complete package license; preserve original grant",
"range": null,
"root": "project",
"sha256": "cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30",
"size": 11358
},
{
"output_sha256": "f4f09e02e4e53be5b49defeab1b6da6cdb9acfe377df892dfef5a0c370a7f88e",
"path": "managed_components/espressif__led_strip/idf_component.yml",
"purpose": "Installed espressif__led_strip 3.0.3 manifest/provenance",
"range": null,
"root": "project",
"sha256": "f4f09e02e4e53be5b49defeab1b6da6cdb9acfe377df892dfef5a0c370a7f88e",
"size": 492
},
{
"output_sha256": "cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30",
"path": "managed_components/espressif__mdns/LICENSE",
"purpose": "espressif__mdns 1.12.0 complete package license; preserve original grant",
"range": null,
"root": "project",
"sha256": "cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30",
"size": 11358
},
{
"output_sha256": "7d2b92cf9ab3055e97a271b98aac3f1044a0224997f419d60a2eda3e411e2ba7",
"path": "managed_components/espressif__mdns/idf_component.yml",
"purpose": "Installed espressif__mdns 1.12.0 manifest/provenance",
"range": null,
"root": "project",
"sha256": "7d2b92cf9ab3055e97a271b98aac3f1044a0224997f419d60a2eda3e411e2ba7",
"size": 536
},
{
"output_sha256": "f878d62e47e693ea64160218edb1b9b4755f4839a3179e5bef4a6b0359399431",
"path": "managed_components/espressif__tinyusb/LICENSE",
"purpose": "espressif__tinyusb 0.21.0~1 complete package license; preserve original grant",
"range": null,
"root": "project",
"sha256": "f878d62e47e693ea64160218edb1b9b4755f4839a3179e5bef4a6b0359399431",
"size": 1094
},
{
"output_sha256": "cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30",
"path": "managed_components/espressif__tinyusb/hw/bsp/espressif/components/led_strip/LICENSE",
"purpose": "Nested TinyUSB LED-strip Apache text, not a separate managed package",
"range": null,
"root": "project",
"sha256": "cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30",
"size": 11358
},
{
"output_sha256": "7ede1ca2d096c412153fe0bf6ac7f66cee1700f0d8c0145fe16572d97107bab3",
"path": "managed_components/espressif__tinyusb/idf_component.yml",
"purpose": "Installed espressif__tinyusb 0.21.0~1 manifest/provenance",
"range": null,
"root": "project",
"sha256": "7ede1ca2d096c412153fe0bf6ac7f66cee1700f0d8c0145fe16572d97107bab3",
"size": 546
},
{
"output_sha256": "d685e441d8dc6e1cde6d8c186d91a75b2ec85996827e4ed3ba788624dfa9fb96",
"path": "managed_components/wolfssl__wolfssh/LICENSE.txt",
"purpose": "wolfssl__wolfssh 1.4.20 complete package license; preserve original grant",
"range": null,
"root": "project",
"sha256": "d685e441d8dc6e1cde6d8c186d91a75b2ec85996827e4ed3ba788624dfa9fb96",
"size": 18561
},
{
"output_sha256": "3f37e0907f74b39690264ddf2c98c4e0450aa124bb3bd83179072db95afbc009",
"path": "managed_components/wolfssl__wolfssh/README.md",
"purpose": "Vendor package licensing evidence; wolfSSH discrepancy deliberately retained",
"range": null,
"root": "project",
"sha256": "3f37e0907f74b39690264ddf2c98c4e0450aa124bb3bd83179072db95afbc009",
"size": 25197
},
{
"output_sha256": "b564e8ff99909fc2a2ac66044fb990be3e4c682ada98d4a70da5586468d55805",
"path": "managed_components/wolfssl__wolfssh/idf_component.yml",
"purpose": "Installed wolfssl__wolfssh 1.4.20 manifest/provenance",
"range": null,
"root": "project",
"sha256": "b564e8ff99909fc2a2ac66044fb990be3e4c682ada98d4a70da5586468d55805",
"size": 814
},
{
"output_sha256": "d5c223b9ed52c38966fcf90ea8c8cf03ea9094912787e3b973148225dc1578a0",
"path": "managed_components/wolfssl__wolfssh/src/internal.c",
"purpose": "Original wolfSSH GPL-3.0-or-later header; package discrepancy evidence",
"range": [
0,
893
],
"root": "project",
"sha256": "81ff1f9166708abd5c2911e9fe57c0aee01c88b5d3f68c909ee8a856d37f36a9",
"size": 510670
},
{
"output_sha256": "d0389a2609e4d4e06bdf706fc2fcf23e393538c2ec445966adbf317affbe3c15",
"path": "managed_components/wolfssl__wolfssh/src/ssh.c",
"purpose": "Original wolfSSH GPL-3.0-or-later header; package discrepancy evidence",
"range": [
0,
792
],
"root": "project",
"sha256": "a4f479ff87eea0980ec1ebdf2c7dd090da473780181b695a56799cb9611f4366",
"size": 92396
},
{
"output_sha256": "7c63cd47453716ce0edef178034cfe64777defb0ae79b22668c528ba657baad8",
"path": "managed_components/wolfssl__wolfssh/wolfssh/internal.h",
"purpose": "Original wolfSSH GPL-3.0-or-later header; package discrepancy evidence",
"range": [
0,
893
],
"root": "project",
"sha256": "8e417149a68f8a6c0506957adf014b3e6c1727a723536826ce5fb0c9e1f1aba3",
"size": 42113
},
{
"output_sha256": "066c056f7d5185db684c04d059fc3e45c4d25193179d3c7ec0736f7a39d92523",
"path": "managed_components/wolfssl__wolfssl/LICENSE.txt",
"purpose": "wolfssl__wolfssl 5.8.2~1 complete package license; preserve original grant",
"range": null,
"root": "project",
"sha256": "066c056f7d5185db684c04d059fc3e45c4d25193179d3c7ec0736f7a39d92523",
"size": 35618
},
{
"output_sha256": "9236deefd68274ba254d6e1baa729aa65f6261047dc8b0b8d1cce923ad39c0fa",
"path": "managed_components/wolfssl__wolfssl/README.md",
"purpose": "Vendor package licensing evidence; wolfSSH discrepancy deliberately retained",
"range": null,
"root": "project",
"sha256": "9236deefd68274ba254d6e1baa729aa65f6261047dc8b0b8d1cce923ad39c0fa",
"size": 31216
},
{
"output_sha256": "7d2a0ee3ce246b64f959358c63e291871228d750ea1bcf82974d8d8934047f84",
"path": "managed_components/wolfssl__wolfssl/idf_component.yml",
"purpose": "Installed wolfssl__wolfssl 5.8.2~1 manifest/provenance",
"range": null,
"root": "project",
"sha256": "7d2a0ee3ce246b64f959358c63e291871228d750ea1bcf82974d8d8934047f84",
"size": 975
},
{
"output_sha256": "f3bc8715bad84b26396bb42d2abd11f919cf58163be158eab79ecbaabf84cdf2",
"path": "third_party/material-design-icons/LICENSE",
"purpose": "Original Pictogrammers summary; per-icon provenance remains open",
"range": null,
"root": "project",
"sha256": "f3bc8715bad84b26396bb42d2abd11f919cf58163be158eab79ecbaabf84cdf2",
"size": 1012
},
{
"output_sha256": "9875d806c9437b5dbdd7ba341beac4772abd8da8e1d14c1a6279fe802b43208b",
"path": "third_party/release-notices/README.md",
"purpose": "Scope, provenance, license choices and unresolved release gates",
"range": null,
"root": "project",
"sha256": "9875d806c9437b5dbdd7ba341beac4772abd8da8e1d14c1a6279fe802b43208b",
"size": 6822
},
{
"output_sha256": "8eb4021ed94ecea9bac60bda1a8be7bbc5ccbfe547196bb061dcbe29c01227de",
"path": "third_party/release-notices/TLSF-BSD-3-Clause.txt",
"purpose": "Project-assembled full TLSF BSD text; see provenance",
"range": null,
"root": "project",
"sha256": "8eb4021ed94ecea9bac60bda1a8be7bbc5ccbfe547196bb061dcbe29c01227de",
"size": 1503
},
{
"output_sha256": "bfb782d9f078b325b3d3cc971a30a19f83d88d3624558055f5cceaaf836c5f6c",
"path": "web_assets/SOURCES.md",
"purpose": "Recorded browser package provenance; not preferred-source delivery",
"range": null,
"root": "project",
"sha256": "bfb782d9f078b325b3d3cc971a30a19f83d88d3624558055f5cceaaf836c5f6c",
"size": 2224
},
{
"output_sha256": "e256f01188af527e4d06d21d06fbf785ae9c50d4b328bf03cbe0ba7f0aa4228f",
"path": "web_assets/addon-fit.LICENSE",
"purpose": "addon-fit 0.10.0 complete MIT notice",
"range": null,
"root": "project",
"sha256": "e256f01188af527e4d06d21d06fbf785ae9c50d4b328bf03cbe0ba7f0aa4228f",
"size": 1103
},
{
"output_sha256": "b569f629d00f2626a8100df2a1798210535621e42164dfd426a6fe5aac7b0ccd",
"path": "web_assets/xterm.LICENSE",
"purpose": "xterm 5.5.0 complete MIT notice",
"range": null,
"root": "project",
"sha256": "b569f629d00f2626a8100df2a1798210535621e42164dfd426a6fe5aac7b0ccd",
"size": 1261
},
{
"output_sha256": "cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30",
"path": "LICENSE",
"purpose": "Complete Apache-2.0 text; not an umbrella license for the SDK or proof of icon provenance",
"range": null,
"root": "sdk",
"sha256": "cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30",
"size": 11358
},
{
"output_sha256": "a97df648b7db77f9f6f5e1ef5fe44eb6b154ecb8ae26ad39c92ea44201cce45c",
"path": "components/console/argtable3/LICENSE",
"purpose": "All aggregate BSD/TCL and other notices, without reduction",
"range": null,
"root": "sdk",
"sha256": "a97df648b7db77f9f6f5e1ef5fe44eb6b154ecb8ae26ad39c92ea44201cce45c",
"size": 8379
},
{
"output_sha256": "efe1692aa5b869edaa0ac88f95eb0276c1125304ffc636a299db3ebbab47f62b",
"path": "components/console/linenoise/LICENSE",
"purpose": "linenoise complete BSD notice",
"range": null,
"root": "sdk",
"sha256": "efe1692aa5b869edaa0ac88f95eb0276c1125304ffc636a299db3ebbab47f62b",
"size": 1408
},
{
"output_sha256": "583218cc7348bba9aa96e80eb6e83ec5beb326b31b08f595ea75a6053fb777f1",
"path": "components/esp-tls/esp_tls_mbedtls.c",
"purpose": "Original opening attribution/license comment(s), exact full-source hash and excerpt",
"range": [
0,
122
],
"root": "sdk",
"sha256": "09210c5a601647ca5775d127a2951bab2f3e509192b53487bbea8a93d8731b78",
"size": 50176
},
{
"output_sha256": "cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30",
"path": "components/esp_coex/lib/LICENSE",
"purpose": "Radio coexistence license; corresponding-source question unresolved",
"range": null,
"root": "sdk",
"sha256": "cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30",
"size": 11358
},
{
"output_sha256": "b7899bfe4f45f8022277af60f5cd23b4d3b564204d8d4388a1aac9a79a4969fb",
"path": "components/esp_http_server/src/httpd_parse.c",
"purpose": "Original opening attribution/license comment(s), exact full-source hash and excerpt",
"range": [
0,
124
],
"root": "sdk",
"sha256": "6bba77064aaa68a06f8d4c01432064a1b050c91ed22741c547785b0d8a6c07d8",
"size": 42937
},
{
"output_sha256": "ac30b81150e83c53ca204dfc012632b5b3d69a0889477e1850067de0f24350c2",
"path": "components/esp_https_server/src/https_server.c",
"purpose": "Original opening attribution/license comment(s), exact full-source hash and excerpt",
"range": [
0,
123
],
"root": "sdk",
"sha256": "6481942b62e51125e2a43441fa0900cbda74bd2ea05c82f0c29eb4933c31946e",
"size": 15257
},
{
"output_sha256": "cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30",
"path": "components/esp_phy/lib/LICENSE",
"purpose": "Radio PHY license; corresponding-source question unresolved",
"range": null,
"root": "sdk",
"sha256": "cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30",
"size": 11358
},
{
"output_sha256": "cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30",
"path": "components/esp_wifi/lib/LICENSE",
"purpose": "Radio Wi-Fi license; corresponding-source question unresolved",
"range": null,
"root": "sdk",
"sha256": "cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30",
"size": 11358
},
{
"output_sha256": "2bb0c0fbfbda95040387540e08f5f5bd811c9a4fc271a4ded940b69e3d868a45",
"path": "components/fatfs/src/ff.c",
"purpose": "Original opening attribution/license comment(s), exact full-source hash and excerpt",
"range": [
0,
944
],
"root": "sdk",
"sha256": "b6511bc0102f42b941897cd9f96f75206b6851fedbebc6dcb17c220cb1d4bede",
"size": 251190
},
{
"output_sha256": "508a77d2e7b51d98adeed32648ad124b7b30241a8e70b2e72c99f92d8e5874d1",
"path": "components/freertos/FreeRTOS-Kernel/LICENSE.md",
"purpose": "FreeRTOS MIT full notice",
"range": null,
"root": "sdk",
"sha256": "508a77d2e7b51d98adeed32648ad124b7b30241a8e70b2e72c99f92d8e5874d1",
"size": 1036
},
{
"output_sha256": "8e3d9f79bbbdf6dfd6fbf132982a7ec3a45ff6dac92277034316efae4cccad84",
"path": "components/freertos/FreeRTOS-Kernel/include/freertos/task.h",
"purpose": "Original opening attribution/license comment(s), exact full-source hash and excerpt",
"range": [
0,
1455
],
"root": "sdk",
"sha256": "ac666bcbe4acb21c44cc820631f8c579d089698967015054c5ab42d092dddc2d",
"size": 133072
},
{
"output_sha256": "968c3f0d60fb9ee8cc1047503823e75ccc28bf6e7e7b0451894b01c1e400d939",
"path": "components/heap/tlsf/README.md",
"purpose": "TLSF author and BSD transition provenance",
"range": null,
"root": "sdk",
"sha256": "968c3f0d60fb9ee8cc1047503823e75ccc28bf6e7e7b0451894b01c1e400d939",
"size": 2960
},
{
"output_sha256": "e4bba79e27dbd0f9e632890a8c8d860222597124d31708166c0105a42a0d411e",
"path": "components/heap/tlsf/include/tlsf.h",
"purpose": "Original opening attribution/license comment(s), exact full-source hash and excerpt",
"range": [
0,
109
],
"root": "sdk",
"sha256": "69799f4f1e7c5f8d3a35e89f5efb8014aa00e6e49f96939b93fc39ceb95d0e8b",
"size": 3063
},
{
"output_sha256": "ea18a0b9b12226900ecc933d075e4c5ebeb3e54a5b0b53e62caab458485be65a",
"path": "components/heap/tlsf/tlsf_block_functions.h",
"purpose": "Original opening attribution/license comment(s), exact full-source hash and excerpt",
"range": [
0,
103
],
"root": "sdk",
"sha256": "c0e8ccac7b83c6964a8e19d83c69e1e2651c25101e34feb36b3e10b96d3fe5a8",
"size": 5502
},
{
"output_sha256": "3aced2d086d3f59cafb6589770c30c1e45a5bdf50c1d4375060b81a615db604c",
"path": "components/http_parser/LICENSE.txt",
"purpose": "HTTP parser full MIT attribution",
"range": null,
"root": "sdk",
"sha256": "3aced2d086d3f59cafb6589770c30c1e45a5bdf50c1d4375060b81a615db604c",
"size": 1298
},
{
"output_sha256": "a36dda207c36db5818729c54e7ad4e8b0c6fba847491ba64f372c1a2037b6d5c",
"path": "components/json/cJSON/LICENSE",
"purpose": "cJSON complete MIT notice; configured group",
"range": null,
"root": "sdk",
"sha256": "a36dda207c36db5818729c54e7ad4e8b0c6fba847491ba64f372c1a2037b6d5c",
"size": 1084
},
{
"output_sha256": "5b649eca17ae68aa41cbe5e4280f724f8dfdd1ed755f8b4c2e73f6ee290f2c54",
"path": "components/lwip/apps/dhcpserver/dhcpserver.c",
"purpose": "Original opening attribution/license comment(s), exact full-source hash and excerpt",
"range": [
0,
122
],
"root": "sdk",
"sha256": "953f46189bc64680ea5fa761e75511fadb3aebf698a0d9dff251d77166d78b80",
"size": 50677
},
{
"output_sha256": "ef4aac92e05e87cd1cdc140870ed52206ba03d4a7fe46c1e11d7ffa6c87d252b",
"path": "components/lwip/lwip/COPYING",
"purpose": "lwIP complete BSD notice",
"range": null,
"root": "sdk",
"sha256": "ef4aac92e05e87cd1cdc140870ed52206ba03d4a7fe46c1e11d7ffa6c87d252b",
"size": 1425
},
{
"output_sha256": "9b405ef4c89342f5eae1dd828882f931747f71001cfba7d114801039b52ad09b",
"path": "components/mbedtls/mbedtls/LICENSE",
"purpose": "Mbed TLS complete dual-license document; Apache option selected",
"range": null,
"root": "sdk",
"sha256": "9b405ef4c89342f5eae1dd828882f931747f71001cfba7d114801039b52ad09b",
"size": 29852
},
{
"output_sha256": "11402351e38392230bb8934ba1095c0c0049a296c0f8821f76e4672dff54b490",
"path": "components/mbedtls/mbedtls/framework/LICENSE",
"purpose": "Nested Mbed TLS framework license",
"range": null,
"root": "sdk",
"sha256": "11402351e38392230bb8934ba1095c0c0049a296c0f8821f76e4672dff54b490",
"size": 29861
},
{
"output_sha256": "fc26fc613803f5910ffe68b6f4924bc833d5613a0fed289f851dd4b6afdf76ea",
"path": "components/mbedtls/mbedtls/library/ssl_tls.c",
"purpose": "Original opening attribution/license comment(s), exact full-source hash and excerpt",
"range": [
0,
223
],
"root": "sdk",
"sha256": "b726c0c55bc5f32255f129d55f9f2fface85ce83de90a2d16c9017b93b738bff",
"size": 329921
},
{
"output_sha256": "0664aa34d3da2b71799883b2fc3250286f76ee69ed0b3676d53d25b635ee721d",
"path": "components/mbedtls/mbedtls/library/x509_create.c",
"purpose": "Original opening attribution/license comment(s), exact full-source hash and excerpt",
"range": [
0,
169
],
"root": "sdk",
"sha256": "fd399239aee30384786a19b47bfe5dd22b979d5d89bb38f29f0c82a3d81daaf7",
"size": 19803
},
{
"output_sha256": "20ed622a5f1a273258b415097609f6a236fb2f23f8c593c63e29e6cc77de0032",
"path": "components/mqtt/esp-mqtt/LICENSE",
"purpose": "ESP-MQTT complete Apache license; configured group",
"range": null,
"root": "sdk",
"sha256": "20ed622a5f1a273258b415097609f6a236fb2f23f8c593c63e29e6cc77de0032",
"size": 11339
},
{
"output_sha256": "0681089a556e93791da82718d68011ba452de245f7f59c3846936304756ac0c0",
"path": "components/newlib/COPYING.NEWLIB",
"purpose": "Complete SDK Newlib mixed notices, separate from toolchain version",
"range": null,
"root": "sdk",
"sha256": "0681089a556e93791da82718d68011ba452de245f7f59c3846936304756ac0c0",
"size": 46295
},
{
"output_sha256": "b8999cb392cc5bbe8cd679de59584ad8d2f26033123e76f1d662fa14b9d4f287",
"path": "components/protobuf-c/protobuf-c/LICENSE",
"purpose": "protobuf-c complete BSD notice; configured group",
"range": null,
"root": "sdk",
"sha256": "b8999cb392cc5bbe8cd679de59584ad8d2f26033123e76f1d662fa14b9d4f287",
"size": 1613
},
{
"output_sha256": "d19257540156a51ed50c2378e7159066415d8bbbe147779f8deb0d3e73f1b0a7",
"path": "components/spiffs/spiffs/LICENSE",
"purpose": "SPIFFS complete MIT notice; configured group",
"range": null,
"root": "sdk",
"sha256": "d19257540156a51ed50c2378e7159066415d8bbbe147779f8deb0d3e73f1b0a7",
"size": 1117
},
{
"output_sha256": "a87ac4e333d0f120408a9d814e40c3672cd27f365af89b4f2f6631f7a9338953",
"path": "components/wpa_supplicant/COPYING",
"purpose": "wpa_supplicant grant redirect and historical licensing context",
"range": null,
"root": "sdk",
"sha256": "a87ac4e333d0f120408a9d814e40c3672cd27f365af89b4f2f6631f7a9338953",
"size": 965
},
{
"output_sha256": "17ccbcf4b17a910795292fb4a95f4742d2ecda37d06fcf20d60ffbd70cfbc609",
"path": "components/wpa_supplicant/README",
"purpose": "wpa_supplicant full current BSD grant",
"range": null,
"root": "sdk",
"sha256": "17ccbcf4b17a910795292fb4a95f4742d2ecda37d06fcf20d60ffbd70cfbc609",
"size": 1803
},
{
"output_sha256": "274cb732d5292ab3fe9a58a7149b447fd3fb43517d9640f074d64dc485c24c98",
"path": "components/xtensa/include/xtensa/hal.h",
"purpose": "Original opening attribution/license comment(s), exact full-source hash and excerpt",
"range": [
0,
1797
],
"root": "sdk",
"sha256": "2e54b026691b4c3b166500f011d99fec5f0824a08dff489dff75c43311010f89",
"size": 64919
},
{
"output_sha256": "1cc05e4ea172de6be2e6b961994de42a0f7433406619f6474811b11066718908",
"path": "package.json",
"purpose": "Installed SDK package 3.50500.0 identity",
"range": null,
"root": "sdk",
"sha256": "1cc05e4ea172de6be2e6b961994de42a0f7433406619f6474811b11066718908",
"size": 536
},
{
"output_sha256": "6474d3ae428762830d36e3b179c6e1622f8f5dc7d32ea94f47be9c122c467979",
"path": "version.txt",
"purpose": "Installed IDF 5.5.0 identity",
"range": null,
"root": "sdk",
"sha256": "6474d3ae428762830d36e3b179c6e1622f8f5dc7d32ea94f47be9c122c467979",
"size": 5
},
{
"output_sha256": "dd464ff1928a475656430e04342a94f0293d2aaf4447bdae438a69c7accc1698",
"path": "package.json",
"purpose": "Installed Xtensa toolchain 14.2.0+20241119 identity; not runtime license classification",
"range": null,
"root": "toolchain",
"sha256": "dd464ff1928a475656430e04342a94f0293d2aaf4447bdae438a69c7accc1698",
"size": 472
},
{
"output_sha256": "9d6b43ce4d8de0c878bf16b54d8e7a10d9bd42b75178153e3af6a815bdc90f74",
"path": "share/licenses/gcc/COPYING.RUNTIME",
"purpose": "Full GCC Runtime Library Exception 3.1; eligibility still requires review",
"range": null,
"root": "toolchain",
"sha256": "9d6b43ce4d8de0c878bf16b54d8e7a10d9bd42b75178153e3af6a815bdc90f74",
"size": 3324
},
{
"output_sha256": "8ceb4b9ee5adedde47b31e975c1d90c73ad27b6b165a1dcd80c7c545eb65b903",
"path": "share/licenses/gcc/gcc/COPYING3",
"purpose": "Full GPLv3 runtime license text",
"range": null,
"root": "toolchain",
"sha256": "8ceb4b9ee5adedde47b31e975c1d90c73ad27b6b165a1dcd80c7c545eb65b903",
"size": 35147
},
{
"output_sha256": "422aa40293093fb54fc66e692a0d68fd0b24ed5602e5d1d33ad05ba3909057e9",
"path": "share/licenses/newlib/COPYING.NEWLIB",
"purpose": "Complete actual toolchain Newlib mixed notices, not the older SDK summary",
"range": null,
"root": "toolchain",
"sha256": "422aa40293093fb54fc66e692a0d68fd0b24ed5602e5d1d33ad05ba3909057e9",
"size": 63318
}
],
"schema": 1,
"snapshot": {
"idf": "5.5.0",
"managed": {
"espressif__esp_tinyusb": "2.2.1",
"espressif__led_strip": "3.0.3",
"espressif__mdns": "1.12.0",
"espressif__tinyusb": "0.21.0~1",
"wolfssl__wolfssh": "1.4.20",
"wolfssl__wolfssl": "5.8.2~1"
},
"reviewed": "2026-09-16",
"sdk_package": "3.50500.0",
"toolchain_package": "14.2.0+20241119"
}
}
+251
View File
@@ -0,0 +1,251 @@
#!/usr/bin/env python3
# SPDX-License-Identifier: GPL-3.0-only
"""Offline, bounded notice assembly for the reviewed installed dependency snapshot."""
import argparse
from contextlib import contextmanager
import hashlib
import json
import os
from pathlib import Path, PurePosixPath
import re
import stat
import sys
CATALOG = "third_party/release-notices/inputs.json"
MAX_FILE = 4 * 1024 * 1024
MAX_TOTAL = 32 * 1024 * 1024
MAX_ENTRIES = 128
ROOTS = {"project", "sdk", "toolchain"}
DIR_FLAGS = os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW | os.O_CLOEXEC
FILE_FLAGS = os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK | os.O_CLOEXEC
INTRO = """Release notice bundle — scoped engineering aid, NOT legal clearance
Start with inputs/project/third_party/release-notices/README.md.
manifest.json records logical input paths, exact source hashes, byte ranges,
and output hashes. No host paths or timestamps are recorded. Texts are retained
verbatim, including mixed grants and the wolfSSH package/header discrepancy.
This is NOT corresponding source, a complete SBOM, an archive of the SDK/tools,
or proof of notice delivery to firmware/device/browser recipients. Radio-blob
corresponding-source/System Library questions remain unresolved. Icon provenance,
wolfSSH packaging clarification, bootloader/runtime attribution, source delivery
and Installation Information still require release review. No legal clearance.
""".encode("utf-8")
class NoticeError(Exception):
"""A failed precondition; never silently omit a required input."""
def digest(data):
return hashlib.sha256(data).hexdigest()
def json_bytes(value):
return (json.dumps(value, indent=2, sort_keys=True, ensure_ascii=False) + "\n").encode("utf-8")
def relative_parts(value):
if not isinstance(value, str) or not value or "\\" in value or "\x00" in value:
raise NoticeError("invalid relative input path")
parts = value.split("/")
if PurePosixPath(value).is_absolute() or any(p in ("", ".", "..") for p in parts):
raise NoticeError("unsafe relative input path")
return parts
def absolute_path(value):
path = Path(value)
if ".." in path.parts:
raise NoticeError("parent traversal is not allowed")
# Do not resolve(): it would hide symlinks from the descriptor walk.
return Path(os.path.abspath(path))
@contextmanager
def directory(path):
"""Pin every directory component, rejecting symlinks (including ancestors)."""
path = absolute_path(path)
fd = os.open(path.anchor, DIR_FLAGS)
try:
for part in path.parts[1:]:
child = os.open(part, DIR_FLAGS, dir_fd=fd)
os.close(fd)
fd = child
yield fd
finally:
os.close(fd)
def read_bounded(root_fd, path, limit=MAX_FILE):
parts = relative_parts(path)
fd = os.dup(root_fd)
try:
for part in parts[:-1]:
child = os.open(part, DIR_FLAGS, dir_fd=fd)
os.close(fd)
fd = child
source = os.open(parts[-1], FILE_FLAGS, dir_fd=fd)
try:
info = os.fstat(source)
if not stat.S_ISREG(info.st_mode) or not 0 < info.st_size <= limit:
raise NoticeError(f"not a nonempty bounded regular file: {path}")
with os.fdopen(source, "rb", closefd=False) as stream:
data = stream.read(limit + 1)
if len(data) != info.st_size or len(data) > limit:
raise NoticeError(f"input changed size or exceeded limit: {path}")
return data
finally:
os.close(source)
finally:
os.close(fd)
def validate_catalog(catalog):
if not isinstance(catalog, dict) or set(catalog) != {"schema", "snapshot", "inputs"}:
raise NoticeError("invalid catalog structure")
if catalog["schema"] != 1 or not isinstance(catalog["snapshot"], dict):
raise NoticeError("unsupported catalog schema")
entries = catalog["inputs"]
if not isinstance(entries, list) or not 0 < len(entries) <= MAX_ENTRIES:
raise NoticeError("invalid catalog input count")
seen = set()
total = 0
for entry in entries:
if not isinstance(entry, dict) or set(entry) != {
"root", "path", "size", "sha256", "range", "output_sha256", "purpose"
}:
raise NoticeError("invalid catalog entry")
if entry["root"] not in ROOTS:
raise NoticeError("unknown input root")
relative_parts(entry["path"])
key = (entry["root"], entry["path"])
if key in seen:
raise NoticeError("duplicate input")
seen.add(key)
size = entry["size"]
if type(size) is not int or not 0 < size <= MAX_FILE:
raise NoticeError("invalid input size")
total += size
for field in ("sha256", "output_sha256"):
if not isinstance(entry[field], str) or not re.fullmatch(r"[0-9a-f]{64}", entry[field]):
raise NoticeError("invalid digest")
span = entry["range"]
if span is not None and (
not isinstance(span, list) or len(span) != 2
or any(type(n) is not int for n in span)
or not 0 <= span[0] < span[1] <= size
):
raise NoticeError("invalid byte range")
if not isinstance(entry["purpose"], str) or not entry["purpose"]:
raise NoticeError("missing input purpose")
if total > MAX_TOTAL:
raise NoticeError("catalog exceeds total read budget")
def assemble(roots, output, catalog_data):
"""Preflight all inputs before creating output. Catalog is trusted reviewed policy."""
if set(roots) != ROOTS:
raise NoticeError("all three input roots are required")
roots = {name: absolute_path(path) for name, path in roots.items()}
output = absolute_path(output)
for root in roots.values():
if output == root or root in output.parents:
raise NoticeError("output must be outside every input root")
if len(catalog_data) > MAX_FILE:
raise NoticeError("catalog exceeds size bound")
catalog = json.loads(catalog_data)
validate_catalog(catalog)
payloads = {}
records = []
for entry in sorted(catalog["inputs"], key=lambda e: (e["root"], e["path"])):
logical = f"{entry['root']}/{entry['path']}"
try:
with directory(roots[entry["root"]]) as root_fd:
source = read_bounded(root_fd, entry["path"], entry["size"])
except OSError as error:
raise NoticeError(f"cannot read required input: {logical} ({error.strerror})") from error
if len(source) != entry["size"] or digest(source) != entry["sha256"]:
raise NoticeError(f"source hash/size mismatch: {logical}; review drift, do not auto-repin")
span = entry["range"]
data = source if span is None else source[span[0]:span[1]]
if digest(data) != entry["output_sha256"]:
raise NoticeError(f"excerpt hash mismatch: {logical}")
try:
data.decode("utf-8")
except UnicodeDecodeError as error:
raise NoticeError(f"non-UTF-8 notice: {logical}") from error
if b"\x00" in data:
raise NoticeError(f"binary notice: {logical}")
target = "inputs/" + logical + (".notice.txt" if span is not None else "")
if target in payloads:
raise NoticeError("output collision")
payloads[target] = data
records.append({**entry, "output": target, "output_size": len(data)})
payloads["README.txt"] = INTRO
manifest = {
"schema": 1,
"snapshot": catalog["snapshot"],
"catalog_sha256": digest(catalog_data),
"inputs": records,
"generated": [{"path": "README.txt", "sha256": digest(INTRO), "size": len(INTRO)}],
}
# Written last as the completion marker. The manifest does not hash itself.
payloads["manifest.json"] = json_bytes(manifest)
with directory(output.parent) as parent_fd:
# Even an empty existing directory or dangling symlink is an error.
os.mkdir(output.name, mode=0o700, dir_fd=parent_fd)
out_fd = os.open(output.name, DIR_FLAGS, dir_fd=parent_fd)
try:
for name, data in payloads.items():
write_new(out_fd, name, data)
finally:
os.close(out_fd)
return manifest
def write_new(root_fd, path, data):
fd = os.dup(root_fd)
try:
parts = relative_parts(path)
for part in parts[:-1]:
try:
os.mkdir(part, mode=0o700, dir_fd=fd)
except FileExistsError:
pass
child = os.open(part, DIR_FLAGS, dir_fd=fd)
os.close(fd)
fd = child
target = os.open(parts[-1], os.O_WRONLY | os.O_CREAT | os.O_EXCL
| os.O_NOFOLLOW | os.O_CLOEXEC, 0o600, dir_fd=fd)
with os.fdopen(target, "wb") as stream:
stream.write(data)
finally:
os.close(fd)
def main(argv=None):
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--project-root", type=Path, default=Path(__file__).absolute().parent.parent)
parser.add_argument("--sdk-root", type=Path, required=True)
parser.add_argument("--toolchain-root", type=Path, required=True)
parser.add_argument("--output", type=Path, required=True,
help="new directory outside input roots; parent must already exist")
args = parser.parse_args(argv)
try:
# No user-supplied catalog, discovery, network, install, build, or repin mode.
with directory(Path(__file__).absolute().parent.parent) as policy_fd:
catalog_data = read_bounded(policy_fd, CATALOG)
manifest = assemble({"project": args.project_root, "sdk": args.sdk_root,
"toolchain": args.toolchain_root}, args.output, catalog_data)
except (NoticeError, OSError, ValueError, TypeError) as error:
print(f"release notices: FAILED: {error}", file=sys.stderr)
return 1
print(f"PASS: {len(manifest['inputs'])} pinned notice/provenance inputs; no release clearance")
return 0
if __name__ == "__main__":
sys.exit(main())
+47 -2
View File
@@ -131,8 +131,9 @@ TLS_POLICY = """ /* mbedTLS retains this pointer: it must outlive every serve
# the input object. Keep original upstream notices verbatim, rather than changing
# their copyright year; the central project modification notice is separate.
# Bounded server parser subset of official wolfSSL/wolfssh PRs 892, 881,
# and 880 (reviewed alongside PR 899). Keep the 1.4.20 state machine and
# password/async edits below. GetSize already uses safe remaining lengths.
# and 880, plus the current-server PR899/902 disposition documented in
# docs/ssh_parser_remaining_review.md. Preserve ordering/password/async edits.
# GetSize already uses safe remaining lengths.
WOLFSSH_PARSER_EDITS = (
Edit("""int GetString(char* s, word32* sSz, const byte* buf, word32 len, word32 *idx)
{
@@ -200,9 +201,36 @@ WOLFSSH_PARSER_EDITS = (
ret = GetString(serviceName, &nameSz, buf, len, &begin);
if (ret != WS_SUCCESS)
return ret;
/* PR902 current-server subset: reject before publishing the transition.
* The owner closes on this error; no best-effort disconnect is queued. */
if (nameSz != sizeof("ssh-userauth") - 1 ||
WMEMCMP(serviceName, "ssh-userauth", sizeof("ssh-userauth") - 1) != 0)
return WS_INVALID_STATE_E;
*idx = begin;
WLOG(WS_LOG_DEBUG, "Requesting service: %s", serviceName);"""),
# PR899 fixes the reversed length predicate. The pinned handler additionally
# needs a bounded recipient parser, not merely the later-tree one-line fix.
Edit(""" if (ssh == NULL || buf == NULL || len != 0 || idx == NULL)
ret = WS_BAD_ARGUMENT;
if (ret == WS_SUCCESS)
ret = WS_CHANOPEN_FAILED;""", """ word32 begin, channelId;
if (ssh == NULL || buf == NULL || idx == NULL)
return WS_BAD_ARGUMENT;
begin = *idx;
ret = GetUint32(&channelId, buf, len, &begin);
if (ret != WS_SUCCESS)
return ret;
if (begin != len)
return WS_BUFFER_E;
if (ChannelFind(ssh, channelId, WS_CHANNEL_ID_SELF) == NULL)
return WS_INVALID_CHANID;
*idx = begin;
ret = WS_CHANOPEN_FAILED;"""),
Edit(""" channel->peerWindowSz += bytesToAdd;
WLOG(WS_LOG_INFO, " update peerWindowSz = %u",
@@ -351,6 +379,17 @@ WOLFSSH_PARSER_EDITS = (
ret = wc_ed25519_verify_msg_init(pk->signature + i, sz,"""),
)
# DoChannelRequest's bounded GetString may truncate at 31 bytes; all recognized
# names are shorter. Exact length first prevents short-name reads and aliases;
# memcmp (not strncmp) also rejects embedded NULs. Keep branch bodies unchanged.
WOLFSSH_PARSER_EDITS += tuple(
Edit(f'WSTRNCMP(type, "{name}", typeSz) == 0',
f'typeSz == sizeof("{name}") - 1 &&\n'
f' WMEMCMP(type, "{name}", sizeof("{name}") - 1) == 0')
for name in ("env", "shell", "exec", "subsystem", "pty-req", "window-change",
"exit-status", "exit-signal", "auth-agent-req@openssh.com")
)
ENTRIES = (
Entry("dhcpserver", "lwip", "idf",
"components/lwip/apps/dhcpserver/dhcpserver.c",
@@ -649,6 +688,12 @@ def render_entry(entry: Entry, roots: dict[str, Path]) -> tuple[Path, bytes]:
" * plus project restricted no-EXT_INFO correction. Provenance and\n"
" * limitations: tools/wolfssh_order/README.md and delta.json.\n"
" */\n")
if entry.name == "wolfssh_internal":
notice += ("/* Server parser review modified 2026-09-16: bounded CHANNEL_FAILURE\n"
" * and ssh-userauth service validation; PR899/902 subset, not full PRs.\n"
" * Local follow-up: exact bounded channel-request names.\n"
" * Provenance/limits: docs/ssh_parser_remaining_review.md.\n"
" */\n")
if entry.header:
notice += ("#if defined(_WOLFSSH_INTERNAL_H_) && \\\n"
" (!defined(SAK_WOLFSSH_ORDER_ABI) || SAK_WOLFSSH_ORDER_ABI != 20260916)\n"