diff --git a/README.md b/README.md index 9270201..e08d325 100644 --- a/README.md +++ b/README.md @@ -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:** 8A–8C 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:** 8A–8C 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 diff --git a/docs/agent/code-map.md b/docs/agent/code-map.md index eb3813c..8d31f65 100644 --- a/docs/agent/code-map.md +++ b/docs/agent/code-map.md @@ -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. diff --git a/docs/agent/current-state.md b/docs/agent/current-state.md index a23c7c5..56bfe2c 100644 --- a/docs/agent/current-state.md +++ b/docs/agent/current-state.md @@ -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. diff --git a/docs/dependency_licenses.md b/docs/dependency_licenses.md index b9b3746..9995ea6 100644 --- a/docs/dependency_licenses.md +++ b/docs/dependency_licenses.md @@ -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 2006–2016; 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 2006–2016; 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 1999–2015; `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. | diff --git a/docs/idf_security_review.md b/docs/idf_security_review.md index 6b8ac4d..5fb0673 100644 --- a/docs/idf_security_review.md +++ b/docs/idf_security_review.md @@ -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:7083–7086`) returns before derivation, and generated X.509 OID allocation (`mbedtls_x509_create/x509_create.c:313–317`) 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:649–659` 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:179–182` 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:412–415` 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:3785–3789` passes `handshake->premaster`; `ssl_misc.h:990` sizes it as `MBEDTLS_PREMASTER_SIZE`, whose union in `ssl.h:691–724` 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.0–3.6.6, not built-in ECC. Original `pk_ecc.c:204–220` has the unchecked `*pub` only under `MBEDTLS_PK_USE_PSA_EC_DATA`; its active `#else` calls `mbedtls_ecp_point_read_binary()`, whose `ecp.c:775–777` checks `ilen < 1`. Application PK parsing **does occur**, in `src/web_security.c:409–414` and `src/ssh_security.c:156–181`, 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:494–555` 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 409–414. Validation checks matching keys, expected subject/issuer, self-signature, and specifically rejects `ca_istrue != 0` at lines 442–444. `src/web_server.c:673–691` 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 4900–5036); 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:4242–4246` still declares uninitialized `tlen`/`lifetime`; failed ticket generation may disclose four stack bytes. `esp_https_server.h:183–192` 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:92–111,128–240`); 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 412–415. **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 3–September 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 2–3, 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: diff --git a/docs/release_packaging.md b/docs/release_packaging.md new file mode 100644 index 0000000..11d7fdc --- /dev/null +++ b/docs/release_packaging.md @@ -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. diff --git a/docs/roadmap.md b/docs/roadmap.md index c047c26..9d300f3 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -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. diff --git a/docs/security_hardening.md b/docs/security_hardening.md index 9fbd191..3994c43 100644 --- a/docs/security_hardening.md +++ b/docs/security_hardening.md @@ -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. 9A–9C 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. diff --git a/docs/ssh_parser_remaining_review.md b/docs/ssh_parser_remaining_review.md new file mode 100644 index 0000000..63bd047 --- /dev/null +++ b/docs/ssh_parser_remaining_review.md @@ -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, 31–65-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. diff --git a/docs/wolf_security_review.md b/docs/wolf_security_review.md index 4e8493e..d9f313d 100644 --- a/docs/wolf_security_review.md +++ b/docs/wolf_security_review.md @@ -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. diff --git a/tests/release_notices/README.md b/tests/release_notices/README.md new file mode 100644 index 0000000..502565d --- /dev/null +++ b/tests/release_notices/README.md @@ -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. diff --git a/tests/release_notices/run.py b/tests/release_notices/run.py new file mode 100644 index 0000000..e912d62 --- /dev/null +++ b/tests/release_notices/run.py @@ -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) diff --git a/tests/wolfssh_parser_contract/README.md b/tests/wolfssh_parser_contract/README.md index 2f660c3..7ac82c1 100644 --- a/tests/wolfssh_parser_contract/README.md +++ b/tests/wolfssh_parser_contract/README.md @@ -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; 31–65-byte names; every header, +name, boolean and payload truncation; nonzero offsets; oversized/wrapping declared +lengths; reply/no-reply; invalid channel and send failure; resize callback absence +and failure. Instrumented comparisons assert the read length equals the initialized +name length, catching removed short-name guards even inside the accessible stack +buffer. Unknown/disabled names must not parse payload fields or invoke callbacks, +but retain existing success/ACK/consumption behavior. + +Real callback/admission cases verify shell-only routing for both roles; null +callback context and absent shell callback; authentication/principal/currentness +requirements; rejected exec/subsystem after a prior shell, even with no reply; +and optional PTY callback behavior. Malformed exec/subsystem payloads still invoke +the existing rejecting callbacks and never become shell admission, matching the +unchanged handler bodies. + +**2,737 cases per stack mode** for each TERM-containing profile, **2,735** for each +of the other two profiles. **18 prefix/length-guard mutations** and **two actual +application admission-gate mutations** are rejected, in addition to the existing +11 parser mutations. The independent whole-source reversal checks that only the +nine predicates change within `DoChannelRequest`, with no branch refactor. + +## Earlier parser test boundaries The C matrix exercises zero/truncated/exact/oversized/wrapping lengths, invalid and nonzero offsets, zero-capacity output, copy canaries, window overflow boundary @@ -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 diff --git a/tests/wolfssh_parser_contract/channel_request.c b/tests/wolfssh_parser_contract/channel_request.c new file mode 100644 index 0000000..231f79f --- /dev/null +++ b/tests/wolfssh_parser_contract/channel_request.c @@ -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 +#include +#include +#include +#include +#include +#include +#include +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;k0); + 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; +} diff --git a/tests/wolfssh_parser_contract/channel_request.py b/tests/wolfssh_parser_contract/channel_request.py new file mode 100644 index 0000000..a7e757d --- /dev/null +++ b/tests/wolfssh_parser_contract/channel_request.py @@ -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) diff --git a/tests/wolfssh_parser_contract/contract.c b/tests/wolfssh_parser_contract/contract.c index fd7a539..81eed94 100644 --- a/tests/wolfssh_parser_contract/contract.c +++ b/tests/wolfssh_parser_contract/contract.c @@ -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]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); diff --git a/tests/wolfssh_parser_contract/pr899.patch b/tests/wolfssh_parser_contract/pr899.patch new file mode 100644 index 0000000..fdd4a4f --- /dev/null +++ b/tests/wolfssh_parser_contract/pr899.patch @@ -0,0 +1,113 @@ +From d2eeec5e263a4821c90805963eeb0666e99868a6 Mon Sep 17 00:00:00 2001 +From: Yosuke Shimizu +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 */ diff --git a/tests/wolfssh_parser_contract/pr902.patch b/tests/wolfssh_parser_contract/pr902.patch new file mode 100644 index 0000000..2a898d5 --- /dev/null +++ b/tests/wolfssh_parser_contract/pr902.patch @@ -0,0 +1,55 @@ +From ffa646a4b9d47d5d9d6127db140c433c58b1e276 Mon Sep 17 00:00:00 2001 +From: Paul Adelsbach +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; diff --git a/tests/wolfssh_parser_contract/pr918.patch b/tests/wolfssh_parser_contract/pr918.patch new file mode 100644 index 0000000..b0efb97 --- /dev/null +++ b/tests/wolfssh_parser_contract/pr918.patch @@ -0,0 +1,307 @@ +From fd82a4bcf55935f0801b14bca6be9c71e32ae914 Mon Sep 17 00:00:00 2001 +From: Yosuke Shimizu +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(); diff --git a/tests/wolfssh_parser_contract/pr919.patch b/tests/wolfssh_parser_contract/pr919.patch new file mode 100644 index 0000000..dce2e87 --- /dev/null +++ b/tests/wolfssh_parser_contract/pr919.patch @@ -0,0 +1,69 @@ +From 0317c40fc131fab952d291d43c56c7b7ce5f4303 Mon Sep 17 00:00:00 2001 +From: Yosuke Shimizu +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(); diff --git a/tests/wolfssh_parser_contract/provenance.json b/tests/wolfssh_parser_contract/provenance.json new file mode 100644 index 0000000..2751bf0 --- /dev/null +++ b/tests/wolfssh_parser_contract/provenance.json @@ -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" + } +} diff --git a/tests/wolfssh_parser_contract/remaining.c b/tests/wolfssh_parser_contract/remaining.c new file mode 100644 index 0000000..f33b178 --- /dev/null +++ b/tests/wolfssh_parser_contract/remaining.c @@ -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); +} diff --git a/tests/wolfssh_parser_contract/review.py b/tests/wolfssh_parser_contract/review.py new file mode 100644 index 0000000..97df1f8 --- /dev/null +++ b/tests/wolfssh_parser_contract/review.py @@ -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) diff --git a/tests/wolfssh_parser_contract/run.py b/tests/wolfssh_parser_contract/run.py index 17be72c..f14f487 100644 --- a/tests/wolfssh_parser_contract/run.py +++ b/tests/wolfssh_parser_contract/run.py @@ -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') diff --git a/third_party/release-notices/README.md b/third_party/release-notices/README.md new file mode 100644 index 0000000..0347549 --- /dev/null +++ b/third_party/release-notices/README.md @@ -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 **2006–2016 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. diff --git a/third_party/release-notices/TLSF-BSD-3-Clause.txt b/third_party/release-notices/TLSF-BSD-3-Clause.txt new file mode 100644 index 0000000..182718c --- /dev/null +++ b/third_party/release-notices/TLSF-BSD-3-Clause.txt @@ -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. diff --git a/third_party/release-notices/inputs.json b/third_party/release-notices/inputs.json new file mode 100644 index 0000000..ff16118 --- /dev/null +++ b/third_party/release-notices/inputs.json @@ -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" + } +} diff --git a/tools/release_notices.py b/tools/release_notices.py new file mode 100644 index 0000000..faf63eb --- /dev/null +++ b/tools/release_notices.py @@ -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()) diff --git a/tools/security_overrides.py b/tools/security_overrides.py index 12c13c0..52b3e28 100644 --- a/tools/security_overrides.py +++ b/tools/security_overrides.py @@ -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"