Apply Phase 9D security mitigations

- Add fail-closed wolfSSL small-math policy and vectors
- Backport DHCP, EMS, and X.509 allocation fixes
- Extend source override validation and operational documentation
This commit is contained in:
2026-09-15 23:06:23 +02:00
parent cdc9c7335a
commit c010e1a1d5
22 changed files with 1562 additions and 38 deletions
+5
View File
@@ -8,9 +8,14 @@ add_compile_definitions(
NO_WOLFSSL_ESP32_CRYPT_AES
NO_WOLFSSL_ESP32_CRYPT_HASH
WOLFSSL_ED25519_STREAMING_VERIFY
# CVE-2025-12888: upstream PR9275 small math policy for enabled algorithms.
# These affect public key layouts; library and consumers must agree.
CURVE25519_SMALL
ED25519_SMALL
)
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
set(PROJECT_VER "0.1.0")
project(esp32_serial_swiss_army_knife)
include(cmake/security_overrides.cmake)
include(cmake/wolf_crypto_policy.cmake)
+7 -4
View File
@@ -14,7 +14,7 @@ ESP32-S3 firmware for a secure, multi-transport RS-232 adapter. It operates one
## Development status
Hardware characterization, serial/USB/Wi-Fi/HTTPS/SSH and local display/control are implemented and hardware-validated. **Phase 8 role-based users and administration is complete:** 8A8C were target-hardware validated and the user explicitly signed off tested firmware at **8D.22 (2026-09-13)**. See the [roadmap](docs/roadmap.md#phase-8--role-based-users-and-administrative-access--complete) and [acceptance evidence](docs/web_administration_acceptance.md). Very low internal/DMA lifetime minima remain a nonblocking headroom follow-up, not an approved reserve. **Phase 9 security hardening is in progress**: 9A crash/debug policy, 9B SSH admission/credential handling and 9C library cleanup/protocol policy have passed host/build checks. Hardware validation is deferred to Phase 9 as a whole. Production readiness is not yet established. See [security hardening](docs/security_hardening.md) for scope, operational profiles, and validation gates.
Hardware characterization, serial/USB/Wi-Fi/HTTPS/SSH and local display/control are implemented and hardware-validated. **Phase 8 role-based users and administration is complete:** 8A8C were target-hardware validated and the user explicitly signed off tested firmware at **8D.22 (2026-09-13)**. See the [roadmap](docs/roadmap.md#phase-8--role-based-users-and-administrative-access--complete) and [acceptance evidence](docs/web_administration_acceptance.md). Very low internal/DMA lifetime minima remain a nonblocking headroom follow-up, not an approved reserve. **Phase 9 security hardening is in progress**: 9A crash/debug policy, 9B SSH admission/credential handling and 9C library cleanup/protocol policy have passed host/build checks. **9D maintenance and lifecycle is in progress:** an operator runbook, 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.
### Browser administration
@@ -33,6 +33,9 @@ Keep UART0 ready for administrative recovery and native USB for network-independ
- [Role-based user database and UART0 administration](docs/user_administration_tests.md): user provisioning and administration, HTTPS/SSH authentication, session revocation, and reusable integrated web-administration regression procedures (not execution evidence).
- [Web administration](docs/web_administration.md): current bounded API/owner and recovery contracts; [acceptance evidence](docs/web_administration_acceptance.md) records sign-off and telemetry limits.
- [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.
- [Command reference](docs/command_reference.md): UART0/admin-SSH administration, serial, broker, USB, Wi-Fi, mDNS, web, SSH, and diagnostic commands.
## Flash partition layout
@@ -64,7 +67,7 @@ pio run --target upload
pio device monitor -b 115200
```
This removes saved serial configuration and all other flash contents. The firmware recreates NVS with safe defaults. Subsequent ordinary uploads do not need a full erase.
**Obtain explicit approval for the target and all flash/NVS data loss before erase.** This removes firmware, accounts, Wi-Fi secrets, server identities and all saved configuration, not just serial settings. Reflash in an isolated RF environment and follow [UART0-first provisioning](docs/security_operations.md#first-provisioning--uart0-first); generated defaults enable fallback Wi-Fi and require deliberate protection. Erase success is not a forensic secure-erasure guarantee. Subsequent ordinary uploads do not need a full erase.
## Build
@@ -87,7 +90,7 @@ The firmware provides an interactive UART0 console at `serial-tool>`. Run `help`
The console supports session history, line editing, cursor movement, and hierarchical Tab completion. After an unattended boot, attach an ANSI-capable terminal and press Enter once to enable enhanced editing; this avoids blocking while no terminal is attached.
Serial, Wi-Fi, and mDNS hostname edits remain in RAM until explicitly saved with `serial save`, `wifi save`, or `mdns save`. Authenticated admin SSH sessions expose the shared operational administration registry, including interactive secrets, TLS/SSH identity management, network diagnostics, and deferred reboot/SSH lifecycle commands. Create the first administrator on UART0 with `user add <username> admin` (optionally `--generate`). Explicit recovery of an unavailable user database remains UART0-only and rebuilds it empty; it refuses a healthy database. An administrator also cannot generate a replacement password for its own account over SSH, preventing the one-time value from being lost when that mutation revokes the session. Legacy web credential commands and `user bootstrap` are removed.
Serial, Wi-Fi, mDNS hostname and display edits remain in RAM until explicitly saved with `serial save`, `wifi save`, `mdns save` or `display save`. These are same-device persistence commands, not backups; see [lifecycle and persistence semantics](docs/security_operations.md#working-configuration-versus-service-lifecycle). Authenticated admin SSH sessions expose the shared operational administration registry, including interactive secrets, TLS/SSH identity management, network diagnostics, and deferred reboot/SSH lifecycle commands. Create the first administrator on UART0 with `user add <username> admin` (optionally `--generate`). Explicit recovery of an unavailable user database remains UART0-only and rebuilds it empty; it refuses a healthy database. An administrator also cannot generate a replacement password for its own account over SSH, preventing the one-time value from being lost when that mutation revokes the session. Legacy web credential commands and `user bootstrap` are removed.
## Security notes
@@ -101,7 +104,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`; review upstream security releases before production use.
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.
### Legacy credential removal
+21 -2
View File
@@ -31,11 +31,29 @@ set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS
# Public extension point: the Python Entry registry supplies the mapping. This
# function is backend-agnostic; a later pinned project/vendor source uses it too.
function(sak_security_replace_source component original generated)
function(sak_security_replace_source component original generated nested_target)
idf_component_get_property(_target "${component}" COMPONENT_LIB)
if(NOT TARGET "${_target}")
message(FATAL_ERROR "Security override: missing component target ${component}")
endif()
if(NOT "${nested_target}" STREQUAL "")
if(NOT component STREQUAL "mbedtls" OR
NOT nested_target MATCHES "^(mbedtls|mbedx509|mbedcrypto)$")
message(FATAL_ERROR "Security override: invalid nested target ${component}/${nested_target}")
endif()
if(NOT TARGET "${nested_target}")
message(FATAL_ERROR "Security override: missing nested target ${nested_target}")
endif()
set(_target "${nested_target}")
get_target_property(_imported "${_target}" IMPORTED)
get_target_property(_alias "${_target}" ALIASED_TARGET)
get_target_property(_owner_dir "${_target}" SOURCE_DIR)
get_filename_component(_owner_dir "${_owner_dir}" REALPATH)
get_filename_component(_expected_owner "${_sak_security_idf}/components/mbedtls/mbedtls/library" REALPATH)
if(_imported OR _alias OR NOT _owner_dir STREQUAL _expected_owner)
message(FATAL_ERROR "Security override: unexpected nested target owner ${_target}: ${_owner_dir}")
endif()
endif()
get_target_property(_source_dir "${_target}" SOURCE_DIR)
get_target_property(_sources "${_target}" SOURCES)
get_filename_component(_expected "${original}" REALPATH)
@@ -98,5 +116,6 @@ foreach(_sak_security_id IN LISTS SAK_SECURITY_OVERRIDE_IDS)
sak_security_replace_source(
"${SAK_SECURITY_${_sak_security_id}_COMPONENT}"
"${SAK_SECURITY_${_sak_security_id}_ORIGINAL}"
"${SAK_SECURITY_${_sak_security_id}_GENERATED}")
"${SAK_SECURITY_${_sak_security_id}_GENERATED}"
"${SAK_SECURITY_${_sak_security_id}_TARGET}")
endforeach()
+9
View File
@@ -0,0 +1,9 @@
# SPDX-License-Identifier: GPL-3.0-only
# Apply after project(). PUBLIC propagates the resolved-settings guard to every
# wolfSSL consumer, including wolfSSH and the application (ABI-sensitive keys).
idf_component_get_property(_sak_wolf_target wolfssl__wolfssl COMPONENT_LIB)
if(NOT TARGET "${_sak_wolf_target}")
message(FATAL_ERROR "wolf crypto policy: missing wolfSSL component target")
endif()
target_compile_options("${_sak_wolf_target}" PUBLIC
"-include${CMAKE_CURRENT_LIST_DIR}/wolf_crypto_policy.h")
+27
View File
@@ -0,0 +1,27 @@
/* SPDX-License-Identifier: GPL-3.0-only */
#ifndef SAK_WOLF_CRYPTO_POLICY_H
#define SAK_WOLF_CRYPTO_POLICY_H
#include <wolfssl/wolfcrypt/settings.h>
/* PR9275 selects small math on Xtensa to avoid compiler-introduced timing
* differences. Check resolved settings, not just command-line intentions.
* https://github.com/wolfSSL/wolfssl/pull/9275
*/
#if !defined(HAVE_CURVE25519) || !defined(CURVE25519_SMALL)
#error "wolf crypto policy: X25519 requires CURVE25519_SMALL"
#endif
#if !defined(HAVE_ED25519) || !defined(ED25519_SMALL)
#error "wolf crypto policy: Ed25519 requires ED25519_SMALL"
#endif
/* 5.8.2 excludes small math from automatic blinding and rejects this pairing.
* Do not force blinding back on: key layout and function signatures differ.
*/
#ifdef WOLFSSL_CURVE25519_BLINDING
#error "wolf crypto policy: small X25519 is incompatible with blinding"
#endif
#if defined(HAVE_CURVE448) || defined(HAVE_ED448)
#error "wolf crypto policy: review PR9275 small math before enabling 448"
#endif
#endif
+3 -2
View File
@@ -17,10 +17,11 @@ This is a semantic map, not a complete file inventory. Start here, then read the
- Files: `src/security_build_policy.c`, registration in `src/CMakeLists.txt`, diagnostic flags in `sdkconfig.defaults`; tests: `tests/security_build_policy/run.py` (optional `--sdkconfig-header` checks the generated configuration).
- Compile-only guard: require no core dumps and silent panic reboot; reject panic/register output, panic/runtime GDB stubs and OCD-aware panic handling. No runtime allocation/task or physical JTAG restriction. Policy, operational profiles and target gates: [Phase 9 hardening](../security_hardening.md).
## Source-pinned dependency corrections (Phase 9C)
## Source-pinned dependency corrections (Phases 9C9D)
- 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 four target sources without modifying SDK/managed components: HTTPS cleanup/private-key release, HTTPD scratch lifetime/null first read, ESP-TLS server-only protocol list, and wolfSSH password bounds/payload wiping. Original notices and compile properties retained; source/hash/target ambiguity fails configuration. Never hand-edit generated copies or silently repin.
- Build input is the exact-hash original **plus checked-in edits**, not installed source alone. Generated copies replace seven target sources without modifying SDK/managed components: HTTPS cleanup/private-key release, HTTPD scratch lifetime/null first read, ESP-TLS server-only protocol list, wolfSSH password bounds/payload wiping, DHCP option bounds (CVE-2026-45160), TLS 1.2 EMS error propagation (CVE-2026-50581), and X.509 OID allocation failure (CVE-2026-34874). The mbedTLS entries explicitly select validated nested `mbedtls`/`mbedx509` targets, not the component wrapper. Original notices and compile properties retained; all seven copies carry the 2026-09-15 modification notice; source/hash/target ambiguity fails configuration. Never hand-edit generated copies or silently repin.
- Xtensa crypto policy: root `CMakeLists.txt` sets `CURVE25519_SMALL`/`ED25519_SMALL` before component parsing; `cmake/wolf_crypto_policy.cmake` PUBLIC-propagates `cmake/wolf_crypto_policy.h` to wolfSSL consumers. The resolved-settings guard requires both small implementations and rejects X25519 blinding and unreviewed Curve448/Ed448 enablement. Tests: `tests/wolf_crypto_policy/run.py`. [Wolf review](../wolf_security_review.md) distinguishes the implemented mitigation from pending ordering/parser/ECC review; [IDF review](../idf_security_review.md) records the three backports and remaining findings.
- Policy/evidence/limits: [library review](../security_library_review.md), [Phase 9C](../security_hardening.md#9c-library-cleanup-and-protocol-policy). Source-contract tests must locate and verify actual generated compilation inputs, not assume original vendor paths.
## Secure randomness
+8
View File
@@ -2,6 +2,14 @@
Working memory, not an implementation timeline. Source is authoritative; begin with [code map](code-map.md), then [architecture](architecture.md) and [decisions](design-decisions.md).
## Phase 9D — advisory mitigation / operational review — 2026-09-15
- Work in progress; user validates Phase 9 as a whole. Secure boot/encrypted NVS excluded. No device operations or dependency upgrades.
- New `security_operations.md`, `dependency_licenses.md`, `wolf_security_review.md`, and `idf_security_review.md` record runbooks, bounded license inventory, and advisory applicability. Phase 9 is not release-ready: wolfSSH message ordering/parser issues and distribution/source-notice questions remain unresolved.
- Current implementation adds consistent Xtensa small X25519/Ed25519 policy and fail-closed compiler guards; seven pinned overrides now include DHCP option bounds, TLS EMS error propagation, and certificate-name allocation failure handling. All generated copies carry dated project modification notices. Nested mbedTLS targets are selected explicitly; installed dependencies remain untouched.
- Parent `pio run` PASS: **94,340 B linked RAM / 1,767,217 B flash** (same RAM, 64,092 B flash vs 9C; not runtime headroom). Parent SDK override suite with actual seven-source registration, strict production wolf crypto policy/vector suite, wolfSSH auth contract135cases, and SSH protocol/context suite all PASS with `CCACHE_DISABLE=1`. Independent review found no actionable implementation defects and repeated the first two suites successfully. DHCP includes196,623 guard-page/equivalence cases; extracted EMS/X509 error tests do not establish full handshake/persistence fault behavior.
- Review docs now distinguish original baseline from implemented mitigations. Seven generated modification/date notices resolved; broader license packaging/source questions remain. No target timing/interoperability, fault-injection or legal-clearance claim. **Next:** coherent wolfSSH message-order/parser correction or reviewed upgrade (1.5.0 candidate, not verified managed compatibility), ECC validation review, remaining advisory inventory and release notices. Do not call Phase9 complete or wait for per-slice target approval.
## Phase 9C — library cleanup / protocol policy — 2026-09-15
- User requested continuation; hardware still deferred to **whole Phase 9**, no per-slice approval gate. Initial Git status clean. Secure boot/encrypted NVS excluded; no eFuse/partition/dependency-version/asset changes, no SDK/managed source mutation.
+188
View File
@@ -0,0 +1,188 @@
# Dependency license inventory — Phase 9D
## Implementation addendum — 2026-09-15
**Missing generated modification/date notices: resolved for all seven generated sources.** `tools/security_overrides.py` prepends a prominent project modification notice dated **2026-09-15**, identifying the editable generator and warning against editing derived copies. Read-only verification matched every existing generated file to its pinned original plus checked-in edits and notice. Both new mbedTLS copies retain their original Mbed TLS Contributors copyright and **`Apache-2.0 OR GPL-2.0-or-later`** SPDX header; the Apache-2.0 selection in this inventory is unchanged. This resolves the narrow notice-generation finding, **not release packaging or license clearance**.
Radio-blob corresponding-source/exception questions, the actual firmware/device/browser notice bundle and delivery, preferred-source packaging, wolfSSH license-package discrepancy, icon provenance/full-license work, and release-specific runtime/bootloader review remain open. Include the new `cmake/wolf_crypto_policy.{cmake,h}` policy inputs with the reproducible build/source package. No upgrades, asset regeneration or device operations were performed for this update.
## Historical inventory baseline — 2026-09-15
The original audit scope and artifact counts below describe the audit-time snapshot; generated-source status and the release checklist are updated explicitly where noted. Original research evidence is retained, not a claim that all artifacts were rebuilt or re-audited here.
Local, bounded audit: **2026-09-15**. This is an engineering inventory and release checklist, not legal advice, a legal compatibility opinion, an upstream currency check, or exhaustive file-by-file coverage. No network requests, dependency changes, asset regeneration, firmware build, tests, or device operations were performed. Hardware validation remains deferred for **Phase 9 as a whole**.
## Evidence and counting boundaries
- Project license: [root LICENSE](../LICENSE), [README license declaration](../README.md#license), and representative `src/` SPDX headers establish **`GPL-3.0-only`**, not “or later.” Dependencies keep their own notices and grants.
- Resolution: [application manifest](../src/idf_component.yml), [dependencies.lock](../dependencies.lock), [platformio.ini](../platformio.ini), all six installed managed manifests and top-level license files.
- **7 lock entries = 6 managed packages + IDF**. Managed packages comprise **5 direct + 1 transitive**. IDF is also a direct requirement. wolfSSH uses wolfSSL in this integration, although its resolved manifest declares no dependency edge; both are explicitly pinned by the application.
- Platform: `platformio/espressif32@6.12.0`; installed SDK package `framework-espidf` **3.50500.0**, SDK **5.5.0**, compiler package `toolchain-xtensa-esp-elf` **14.2.0+20241119**. Installed package metadata, not merely requested ranges, was checked.
- Existing `.pio/build/esp32-s3-devkitc-1-n16r8/project_description.json` names **113 nonempty configured components**. Its adjacent `esp32_serial_swiss_army_knife.map` has **68 distinct archive basenames** in the initial “Archive member included” region, before “Discarded input sections.” These are build-selection evidence, **not** 113 independently audited packages or proof all selected bytes survive section garbage collection. These pre-existing artifacts were not rebuilt or certified fresh.
- Below: **6 managed rows, 18 SDK/runtime inventory groups, 2 browser packages, and 1 icon collection containing 2 SVGs**. Groups are not a complete SBOM and must not be summed into a unique package count (e.g. bundled libraries are inside IDF).
- [Web provenance](../web_assets/SOURCES.md): all **8 recorded SHA-256 values** matched (3 JS/CSS, 2 license files, 3 gzip artifacts). Two SVG IDs and the attribution in `src/local_status_ui.c:158161` were inspected for provenance only. Logo ownership is the project's recorded claim, not independently established authorship.
- Initial Git status was clean. An unrelated untracked `docs/security_operations.md` appeared during this audit and was left unread and untouched. This audit creates only this file.
### Local path notation
Repository paths below are relative to the repository root. Installed paths are expressed using these **audit-time roots**, not environment variables that must already be set:
- `SDK/` = `/home/mscholz/.platformio/packages/framework-espidf/`
- `TC/` = `/home/mscholz/.platformio/packages/toolchain-xtensa-esp-elf/`
- `BUILD/` = `.pio/build/esp32-s3-devkitc-1-n16r8/`
`SDK/package.json` identifies <https://github.com/espressif/esp-idf>; `SDK/version.txt` says `5.5.0`. `SDK/LICENSE` is Apache-2.0. **There is no `SDK/COPYRIGHT` in this installed package**, and its `README.md` contains no license/copyright section. Do not assume the root Apache text covers every bundled library or is a complete attribution list. Component license files and source notice headers supply important exceptions.
## Managed components: complete lock-level inventory
All rows have a checked `managed_components/<directory>/idf_component.yml`. Versions below match the lock and installed manifests. Exact component hashes remain in `dependencies.lock`; a manifest-only assertion is not a cryptographic revalidation of all installed package contents.
| Package / resolved version | Relationship | Local license evidence | SPDX interpretation / disposition |
| --- | --- | --- | --- |
| `espressif/esp_tinyusb` **2.2.1** | Direct; requires TinyUSB | `managed_components/espressif__esp_tinyusb/LICENSE` | `Apache-2.0`; retain notices and license. |
| `espressif/led_strip` **3.0.3** | Direct | `managed_components/espressif__led_strip/LICENSE` | `Apache-2.0`. |
| `espressif/mdns` **1.12.0** | Direct; requested `^1.8.2` | `managed_components/espressif__mdns/LICENSE` | `Apache-2.0`; inventory resolved 1.12.0, not minimum 1.8.2. |
| `espressif/tinyusb` **0.21.0~1** | Transitive through esp_tinyusb | `managed_components/espressif__tinyusb/LICENSE` | `MIT`, copyright 20122026 hathach. Nested `hw/bsp/espressif/components/led_strip/LICENSE` is Apache-2.0: it is not a seventh lock package. Retain nested notices if distributing the full package. |
| `wolfssl/wolfssl` **5.8.2~1** | Direct | `managed_components/wolfssl__wolfssl/LICENSE.txt`, `README.md`, `wolfssl/version.h` | `GPL-3.0-or-later` open-source option; GPLv3 can be selected with this project. The commercial alternative is not needed merely because the application is GPLv3-only. |
| `wolfssl/wolfssh` **1.4.20** | Direct | `managed_components/wolfssl__wolfssh/LICENSE.txt`, `README.md`, `src/internal.c` opening notice, `wolfssh/version.h` | **Packaging discrepancy:** LICENSE/README describe *wolfSSL/wolfCrypt*, GPLv2-or-later, and include GPLv2 text; inspected wolfSSH headers explicitly grant **`GPL-3.0-or-later`**. Preserve both evidence sets; select GPLv3 for this integration, and seek upstream packaging clarification before release. Do not label wolfSSH GPL-2.0-only or silently rewrite vendor files. |
Provenance references (not fetched): installed manifests point to [esp-usb](https://github.com/espressif/esp-usb), [esp-protocols](https://github.com/espressif/esp-protocols), [Espressif TinyUSB](https://github.com/espressif/tinyusb), [wolfSSL](https://github.com/wolfSSL/wolfssl), and [wolfSSH](https://github.com/wolfSSL/wolfssh). Installed repository metadata pins esp_tinyusb commit `8e779566ef71d43928cbf7e125e8eb54bab3f542`, mDNS `db06b19b7be729c163d346f62ec0eba01047b7f1`, and TinyUSB `7049c58a0e895acc92c6407574b05b5536eddfc8`. Use the LED strip manifest's repository metadata rather than infer a repository from its package name.
## SDK core, bundled libraries, and compiler runtime
“Selected” means archive-selection evidence in the existing application map, not a full object/license attribution trace. Unversioned rows mean **the installed IDF 5.5.0 snapshot**, not an invented upstream release. Header versions do not identify every Espressif patch; preserve the resolved SDK package itself.
| Group / version evidence | Checked local license / notice paths | SPDX / scope and action |
| --- | --- | --- |
| IDF core and Espressif drivers **5.5.0** | `SDK/LICENSE`, `SDK/package.json`, `SDK/version.txt`; original notices retained in the SDK overlays below | `Apache-2.0` baseline, not an umbrella claim over bundled code. Core archives selected. Include attribution and relevant notices, including bootloader distribution inputs. |
| FreeRTOS **10.5.1**, Espressif port | `SDK/components/freertos/FreeRTOS-Kernel/LICENSE.md`; version in `include/freertos/task.h` under that kernel directory | `MIT`; selected. License text does not substitute for copyright notices in individual files. |
| lwIP **2.2.0 development**, Espressif fork | `SDK/components/lwip/lwip/COPYING`; `src/include/lwip/init.h` under that directory has `LWIP_RC_DEVELOPMENT` | `BSD-3-Clause`; selected. Preserve Swedish Institute of Computer Science attribution; do not call it stock release 2.2.0. |
| Mbed TLS **3.6.3** | `SDK/components/mbedtls/mbedtls/LICENSE`; `include/mbedtls/build_info.h` under that directory | `Apache-2.0 OR GPL-2.0-or-later`; choose Apache-2.0 here. `libmbedtls.a`, `libmbedcrypto.a`, `libmbedx509.a` selected. Framework has a separate `framework/LICENSE` when shipping the full source tree. |
| HTTP parser **2.7.0** | `SDK/components/http_parser/LICENSE.txt`, `http_parser.h` in that component | `MIT`; selected. Preserve NGINX/Igor Sysoev and Joyent/Node attribution in the actual text. |
| argtable3, IDF snapshot | `SDK/components/console/argtable3/LICENSE` | **Aggregate**, including `BSD-3-Clause`, `BSD-2-Clause`, and `TCL` terms. It includes Stewart Heitmann, NetBSD getopt, Tcl, C Hash Table, and Better String notices. Do not reduce the entire file to one BSD label. `libconsole.a` selected; sublibrary/object attribution not exhaustively traced. |
| linenoise, IDF snapshot | `SDK/components/console/linenoise/LICENSE` | `BSD-2-Clause`, Salvatore Sanfilippo and Pieter Noordhuis; console selected. |
| TLSF allocator, IDF snapshot | `SDK/components/heap/tlsf/include/tlsf.h`, `SDK/components/heap/tlsf/tlsf_block_functions.h` | `BSD-3-Clause` SPDX headers, Matthew Conte 20062016; heap selected. No standalone TLSF license text appeared in the license-file inventory; obtain/assemble the correct full notice for release, not a generic BSD text with another author's name. |
| Xtensa HAL, IDF snapshot | `SDK/components/xtensa/include/xtensa/hal.h` opening notice | `MIT`-style full permission notice, Cadence 19992015; `libxt_hal.a`/`libxtensa.a` selected. Header evidence is not an audit of every HAL object or ROM implementation. |
| Espressif prebuilt radio libraries, IDF snapshot | `SDK/components/esp_wifi/lib/LICENSE`, `SDK/components/esp_phy/lib/LICENSE`, `SDK/components/esp_coex/lib/LICENSE` | All three files are byte-identical to `SDK/LICENSE` (`Apache-2.0`). Map selects Wi-Fi `libcore.a`, `libespnow.a`, `libmesh.a`, `libnet80211.a`, `libpp.a`, and PHY `libbtbb.a`, `libphy.a`. `libesp_coex.a` is selected but that does not prove `libcoexist.a` was selected. **Corresponding-source/system-library question remains open**, despite permissive binary redistribution terms. |
| Newlib **4.3.0** and IDF libc integration | `TC/xtensa-esp-elf/include/_newlib_version.h`; `TC/share/licenses/newlib/COPYING.NEWLIB`; `SDK/components/newlib/COPYING.NEWLIB` | Mixed per-file licenses; **no single SPDX expression established**. Toolchain notice file has 1,293 lines vs SDK copy's 952; preserve the actual runtime package's notices, not just the older SDK summary. `libc.a` and `libnewlib.a` selected. Target-specific LGPL/GPL sections for other architectures/Linux are not proof they apply to ESP32-S3; trace selected objects before making that claim. |
| GCC runtime / libstdc++ **14.2.0**, package **14.2.0+20241119** | `TC/share/licenses/gcc/COPYING.RUNTIME`, `TC/share/licenses/gcc/gcc/COPYING3`; `TC/xtensa-esp-elf/include/c++/14.2.0/xtensa-esp-elf/esp32s3/bits/c++config.h` | Header explicitly supports `GPL-3.0-or-later WITH GCC-exception-3.1`; `libgcc.a`/`libstdc++.a` selected. Check exception eligibility and per-object terms; preserve exception and GPL notices. Package-level `GPL-2.0-or-later` metadata is not a runtime license inventory. `COPYING3.LIB` is LGPL text, **not** the GCC runtime exception. |
| cJSON **1.7.18** | `SDK/components/json/cJSON/LICENSE`, `cJSON.h` there | `MIT`; configured SDK component, no `libjson.a` selection in the inspected initial map region. Retain when distributing the SDK source; not asserted live in this image. |
| FatFs **R0.15 w/patch2** | `SDK/components/fatfs/src/ff.c` opening full notice | `LicenseRef-FatFs-local-notice` (descriptive local identifier, not an official SPDX ID): source-retention permission/disclaimer, ChaN 2022. Configured, no `libfatfs.a` selection observed. |
| SPIFFS, IDF snapshot | `SDK/components/spiffs/spiffs/LICENSE` | `MIT`, Peter Andersson; configured, no `libspiffs.a` selection observed. |
| protobuf-c, IDF snapshot | `SDK/components/protobuf-c/protobuf-c/LICENSE` | `BSD-2-Clause`; configured, no `libprotobuf-c.a` selection observed. |
| ESP-MQTT, IDF snapshot | `SDK/components/mqtt/esp-mqtt/LICENSE` | `Apache-2.0`; configured, no `libmqtt.a` selection observed. |
The eighteenth group is **wpa_supplicant**, recorded separately because its license requires reading two files: `SDK/components/wpa_supplicant/COPYING` redirects to `SDK/components/wpa_supplicant/README`, whose full grant is **`BSD-3-Clause`**. The historical GPLv2 option was retired in 2012 according to COPYING; do not flag a GPLv2-only conflict. `libwpa_supplicant.a` is selected; an independent upstream version was not established (use the IDF snapshot).
The installed license-path inventory also found OpenThread, NimBLE/Bluetooth, micro-ecc, Unity/CMock, and tool/test-specific licenses and NOTICE files. They were not promoted to fully reviewed linked dependencies. A full SDK/source or toolchain redistribution must preserve their applicable texts too. No inference that all of these are absent from every bootloader, ROM, or future build is made. The bootloader map, every bundled file, and every selected runtime object were not audited. Newlib, argtable3, radio blobs, ROM attribution, and nested package contents remain aggregation boundaries needing release-specific review.
## Checked-in assets and existing notice sufficiency
| Asset | Version / local evidence | Finding |
| --- | --- | --- |
| `@xterm/xterm` | **5.5.0**; `web_assets/xterm.js`, `xterm.css`, `xterm.LICENSE`; [SOURCES.md](../web_assets/SOURCES.md) provides exact npm URLs/hashes | `MIT`. Full notice retained in repository; CSS also retains a notice. Minified JS has no `copyright`, `permission is hereby`, or `license` marker. |
| `@xterm/addon-fit` | **0.10.0**; `web_assets/addon-fit.js`, `addon-fit.LICENSE`; same provenance document | `MIT`. Full notice retained in repository; minified JS lacks those notice markers. |
| Pictogrammers Material Design Icons | **7.4.47 according to `src/local_status_ui.c`**; `third_party/material-design-icons/usb.svg`, `wifi-strength-4.svg`, `LICENSE` | Source comments identify hand-rasterized USB and adapted Wi-Fi masks as Apache-2.0 icon derivatives. Retained license is **“Pictogrammers Free License” summary**, referring icons to Apache-2.0 or their respective licenses, code to MIT. **Not the full Apache-2.0 text or a per-icon origin record.** Version/individual-icon licensing is not independently authenticated by a local package manifest. |
| Project logo | `web_assets/logo.png`, derived from `images/logo.png` per SOURCES.md | Project-owned according to provenance document; no independent third-party license assigned. Generator's MIT banner must not be treated as proof that the logo was separately relicensed MIT. |
`git ls-files '*LICENSE*' '*COPYING*' '*NOTICE*'` found **4 tracked license files**: root GPLv3, two web MIT texts, and the icon summary. Existing `third_party/` licensing is therefore **not sufficient as a complete release notice bundle**. It also does not supply the missing full Apache text for the icon derivatives.
`web_assets/generate_embedded_assets.py` embeds three gzip files and the logo, **not either MIT license file**. Its generated-source banner points back to provenance/licenses; it is not the upstream MIT notice itself. This proves the embedding path does not carry the separate notice texts, not that every possible product-delivery channel lacks notices. Coordinate a release notice bundle and a way for recipients of the browser assets to obtain the notices (for example a bundled/served third-party license resource); verify the actual delivery path. Do not assume repository-only notices accompany a standalone firmware/device or a downloaded JS response. No assets or web endpoints were changed in this audit.
Preferred-source availability is a separate issue from retaining MIT notices. For a GPL-covered combined release, preserve exact upstream preferred-form browser sources and build inputs where required; the checked-in minified JS distributions and gzip/C arrays are not automatically the preferred form for modification. Source-map references alone do not provide that source. Whether independent browser packages are mere aggregation should be assessed for the actual distribution, not assumed either way.
## GPLv3 distribution and build-overlay obligations
The inspected MIT, BSD and Apache-2.0 grants provide generally GPLv3-compatible routes; the project's `GPL-3.0-only` choice can use wolfSSL/wolfSSH's version-3 alternatives. This is **not** permission to remove upstream notices or declare all files GPL-only. No commercial wolfSSL/wolfSSH license is recommended on the evidence here. Commercial arrangements would be a separate decision if a planned distribution cannot satisfy the available open-source terms, not a cure for unrelated project/SDK obligations.
For conveyance, review GPLv3 §§1, 46, 7 and 10 in [LICENSE](../LICENSE):
1. Supply required license/copyright/warranty notices and use a §6-compliant corresponding-source delivery method for binaries. A dependency name, registry URL, generic upstream source link, or this inventory alone is not corresponding source or a compliant written offer.
2. Preserve exact application source, lock/manifests, relevant SDK and managed source, configuration/build/link inputs, asset preferred sources and generation scripts. Include the scripts controlling compilation/installation. Generic unmodified build tools may fall outside corresponding source, but record precise versions needed for reproduction; if distributing those tools themselves, satisfy their separate licenses.
3. The Phase 9C firmware compiles **pinned originals plus project edits**. Include `tools/security_overrides.py`, `cmake/security_overrides.cmake`, root CMake integration, original hash-matching sources, and the means to reproduce the generated copies. A stock IDF/wolfSSH tarball alone omits the compiled modifications. Generated copies alone are not a substitute for the preferred editable patch/generator inputs.
4. **Resolved 2026-09-15:** all seven generated files below retain upstream notices and now carry the explicit project modification/date notice from the generator. The historical four-file audit found the notice missing; this narrow defect is repaired. GPLv3 §5(a) calls for a relevant date, and Apache-2.0 §4(b) requires prominent change notices. Continue including notices and reproducible generator inputs in distribution; do not hand-edit generated copies or vendor sources.
5. If distributing a GPLv3 “User Product,” assess and provide required Installation Information under §6. Do not publish device passwords, private keys, Wi-Fi material, account verifiers, tickets, NVS images, or real credential backups as build/source artifacts. Installation compliance should use appropriate documented procedures, not routine secret disclosure. Secure boot was not enabled or altered here.
6. **Prebuilt radio libraries are the principal unresolved compatibility/source risk.** Apache-2.0 permits binary redistribution, but does not itself satisfy GPL corresponding-source obligations for a combined firmware. Establish whether required preferred source is obtainable, or whether a defensible GPL System Library/other exception applies to the actual linked artifacts. Static linkage is not automatically “mere aggregation.” Do not declare distribution cleared or definitely prohibited from this bounded audit. Escalate this before a public binary/device release.
Checked generated paths and retained licenses:
- `BUILD/security_overrides/https_server/https_server.c` — Espressif Apache-2.0.
- `BUILD/security_overrides/httpd_parse/httpd_parse.c` — Espressif Apache-2.0.
- `BUILD/security_overrides/esp_tls_mbedtls/esp_tls_mbedtls.c` — Espressif Apache-2.0.
- `BUILD/security_overrides/wolfssh_internal/internal.c` — wolfSSH GPL-3.0-or-later header.
- `BUILD/security_overrides/dhcpserver/dhcpserver.c` — retained Espressif Apache-2.0 header.
- `BUILD/security_overrides/mbedtls_ssl_tls/ssl_tls.c` — retained Mbed TLS Contributors, `Apache-2.0 OR GPL-2.0-or-later`.
- `BUILD/security_overrides/mbedtls_x509_create/x509_create.c` — retained Mbed TLS Contributors, `Apache-2.0 OR GPL-2.0-or-later`.
For Apache-2.0 components, retain required attribution and any applicable upstream NOTICE contents under §4(d), **if present**; do not fabricate NOTICE text or assert every Apache package must have a NOTICE file. For MIT/BSD components, distribute the actual copyright, permission/conditions and disclaimer, not merely an SPDX name. Preserve mixed notices such as argtable3 and Newlib rather than replacing them with generic templates.
## Actionable release work (not performed)
| 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. |
| Resolved in generator; retain in distribution | Prominent modification notices dated **2026-09-15** now appear in **all 7 generated files**, with original notices retained. Release packaging must still include them and the reproducible patch/build inputs. |
| 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. |
## Minimal offline repeat check
Run from the repository root against an already-resolved local environment. This only reads manifest/license/provenance metadata; it does **not** invoke PlatformIO, install dependencies, regenerate assets, read configuration secrets, or validate legal compliance. It requires Python 3 and PyYAML (the latter is normally available in the component-manager environment). A missing prerequisite must be reported, not automatically downloaded. Adjust the two package roots if auditing another machine.
```sh
python3 - <<'PY'
from pathlib import Path
import hashlib
import json
import re
import yaml
sdk = Path('/home/mscholz/.platformio/packages/framework-espidf')
tc = Path('/home/mscholz/.platformio/packages/toolchain-xtensa-esp-elf')
expected = {
'espressif/esp_tinyusb': ('2.2.1', 'LICENSE'),
'espressif/led_strip': ('3.0.3', 'LICENSE'),
'espressif/mdns': ('1.12.0', 'LICENSE'),
'espressif/tinyusb': ('0.21.0~1', 'LICENSE'),
'wolfssl/wolfssh': ('1.4.20', 'LICENSE.txt'),
'wolfssl/wolfssl': ('5.8.2~1', 'LICENSE.txt'),
}
lock = yaml.safe_load(Path('dependencies.lock').read_text())
assert set(lock['dependencies']) == set(expected) | {'idf'}
assert str(lock['dependencies']['idf']['version']) == '5.5.0'
assert sdk.joinpath('version.txt').read_text().strip() == '5.5.0'
assert json.loads(sdk.joinpath('package.json').read_text())['version'] == '3.50500.0'
assert json.loads(tc.joinpath('package.json').read_text())['version'] == '14.2.0+20241119'
for name, (version, license_name) in expected.items():
directory = Path('managed_components') / name.replace('/', '__')
manifest = yaml.safe_load((directory / 'idf_component.yml').read_text())
assert str(lock['dependencies'][name]['version']) == version
assert str(manifest['version']) == version
license_path = directory / license_name
assert license_path.stat().st_size > 0
print(name, version, license_path,
hashlib.sha256(license_path.read_bytes()).hexdigest())
for name, digest in re.findall(r'\| `([^`]+)` \| `([a-f0-9]{64})`',
Path('web_assets/SOURCES.md').read_text()):
assert hashlib.sha256((Path('web_assets') / name).read_bytes()).hexdigest() == digest
for path in [Path('LICENSE'), Path('third_party/material-design-icons/LICENSE'),
sdk / 'LICENSE', sdk / 'components/wpa_supplicant/README',
sdk / 'components/console/argtable3/LICENSE',
sdk / 'components/newlib/COPYING.NEWLIB',
tc / 'share/licenses/newlib/COPYING.NEWLIB',
tc / 'share/licenses/gcc/COPYING.RUNTIME']:
assert path.stat().st_size > 0
print(path, hashlib.sha256(path.read_bytes()).hexdigest())
for name, digest in {
'dependencies.lock': '2d1c62093cafdb54cd209813c526c35e2e470b62cc6f5b0cde9a040569662726',
'src/idf_component.yml': '7fe8cfd25db36dd23907cd1aa7645f3d5b5f34f7ed587cd2599690b5ffbda053',
'platformio.ini': 'b0139667b13c0e254b5ffa67df6cb323f4554a3657fb100330067d6110d57518',
}.items():
assert hashlib.sha256(Path(name).read_bytes()).hexdigest() == digest, name
print('PASS: scoped manifest versions, notice presence, and recorded hashes')
PY
```
This is a **drift tripwire**, not a full component-content verifier or complete license scanner. On changes, inspect new notices and nested components, trace final application/bootloader maps, revisit the build overlays and regenerate a release inventory deliberately. Do not simply repin expected values. Network/upstream authentication, advisory review, legal aggregation analysis, complete notice assembly and hardware validation remain outside this check.
+185
View File
@@ -0,0 +1,185 @@
# Focused installed ESP-IDF security applicability review
## 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:
- **CVE-2026-45160:** pinned DHCP backport handles PAD/END before length access, validates remaining payload length and minimum type/IP lengths, and advances by the validated length. Remaining-length comparisons deliberately avoid forming out-of-object pointers.
- **CVE-2026-50581:** TLS 1.2 EMS `calc_verify` failure now returns `ret` before derivation. This is the TLS 1.2 hunk only, not the upstream TLS 1.3 correction or a global mbedTLS update.
- **CVE-2026-34874:** X.509 OID allocation failure now returns `MBEDTLS_ERR_X509_ALLOC_FAILED` before `memcpy()`.
- The registry now has **seven** entries: the four historical entries below plus `dhcpserver` on `lwip`, `mbedtls_ssl_tls` on nested target `mbedtls`, and `mbedtls_x509_create` on nested target `mbedx509`. Nested-target selection validates the allowlist and owning SDK library directory, rejects imported/alias targets, and retains the exactly-one-source/property-preservation checks. All seven generated copies were read-only verified against rendered pinned inputs; each carries the project modification notice dated **2026-09-15**, retaining upstream notices.
- **Still unpatched:** CVE-2026-45541 WS negotiation and CVE-2025-48965 ASN.1 named-data state. Their baseline applicability qualifications below remain; the three backports do not fix them.
Supplied parent evidence: `pio run` **PASS**, **94,340 B linked RAM / 1,767,217 B flash** (unchanged RAM, **64,092 B flash** from the 9C 1,831,309 B baseline; includes the wolf crypto policy). Parent reports all four focused commands in [hardening validation](security_hardening.md#mitigation-hostbuild-evidence--2026-09-15) passed; independent reviewer reports the SDK-override and wolf-crypto-policy suites passed with no blocking implementation defects. These commands were not rerun for this documentation update. No target evidence, dependency upgrade, asset regeneration or device operation is claimed. Broader advisory review and whole-phase acceptance remain open.
## Historical pre-mitigation research baseline — 2026-09-15
**The remainder preserves the original investigation, hashes, applicability and proposed tests. Statements such as “not implemented,” “current,” four entries and original compilation inputs describe that earlier snapshot, not the implementation above.**
Review date: **2026-09-15**. Scope: the installed `/home/mscholz/.platformio/packages/framework-espidf`, current application source, and checked-in/generated security overrides. **Review and recommendations only; no fixes implemented.** No build, test compilation, upload, monitor, erase, or device operations were performed.
This is not a complete IDF/mbedTLS advisory inventory, certification, or statement that an old version is safe. Advisory release tables are not sufficient to identify downstream backports: the installed implementation and actual source registration were inspected.
## Decision summary
| Issue | Confirmed local finding | Application applicability | Recommended priority |
| --- | --- | --- | --- |
| CVE-2026-45160, DHCP option parsing | Original vulnerable parser compiled; no override | Exposed whenever the project SoftAP/DHCP server is active; adjacent associated client, no application login required | **First correction:** backport the release/v5.5 bounds fix |
| CVE-2026-45541, WS subprotocol parsing | Vulnerable SDK function compiled; no override | Faulty tokenization loop **not reached by current application upgrades**, which pass `NULL`; both routes are ordinary HTTP handlers | Defense-in-depth backport; preserve current admission-before-101 design |
| CVE-2026-50581, ignored EMS error | Missing early return in installed `ssl_compute_master()` | TLS 1.2/EMS path used by HTTPS; hash/allocation failure prerequisite. Cipher restriction and disabled renegotiation do not remove this path | Prompt fail-closed correction alongside DHCP/X.509 work |
| CVE-2025-48965, ASN.1 named-data state | Zero-length replacement clears pointer but leaves stale length | Current certificate names are trusted, single nonempty CNs; malicious repeated-OID/empty-value trigger not provided by current name generation | Lower-priority library correction/regression coverage |
| CVE-2026-34874, X.509 name allocation failure (3.6.6 fix) | Unchecked allocation immediately followed by `memcpy()` | **Used by local certificate creation**, even with the safe fixed CN; OOM can trigger it | Prompt correction; fixed input is not a mitigation |
**Important correction to issue classification:** CVE-2025-48965 is not the X.509 allocation-failure bug. Its official advisory describes a stale `val.len` after a zero-length replacement. CVE-2026-34874 is the separate unchecked allocation fixed in 3.6.6. Do not merge their applicability or mark either fixed by the project's HTTPD scratch allocator changes.
## Exact reviewed baseline and registration
Installed `components/esp_common/include/esp_idf_version.h` defines **5.5.0**. `components/mbedtls/mbedtls/include/mbedtls/build_info.h` defines **3.6.3**, `0x03060300`. The following SHA-256 values were calculated from installed bytes, not inferred from version labels. Paths are relative to the installed IDF root:
| Installed source | SHA-256 |
| --- | --- |
| `components/esp_common/include/esp_idf_version.h` | `30fdfbed6cc0055a89f3454f61b56d5e5643bbc46c0774aae63e41869cf2947a` |
| `components/lwip/apps/dhcpserver/dhcpserver.c` | `953f46189bc64680ea5fa761e75511fadb3aebf698a0d9dff251d77166d78b80` |
| `components/esp_http_server/src/httpd_ws.c` | `e514f603a696e3e775573bc8882a350691e31fcae96e435f9062248e7c91677c` |
| `components/mbedtls/mbedtls/library/ssl_tls.c` | `b726c0c55bc5f32255f129d55f9f2fface85ce83de90a2d16c9017b93b738bff` |
| `components/mbedtls/mbedtls/library/asn1write.c` | `6667aebb1e5f0500ef4a281fe2752fc7bc59610e7bfc01d2916d224e5bc5c3e3` |
| `components/mbedtls/mbedtls/library/x509_create.c` | `fd399239aee30384786a19b47bfe5dd22b979d5d89bb38f29f0c82a3d81daaf7` |
### Checked-in edits are part of the effective source
Root `CMakeLists.txt:1516` includes `cmake/security_overrides.cmake` after `project()`. That file invokes `tools/security_overrides.py`, loads its manifest, and replaces exactly one source on the resolved component target, preserving source properties and original quoted-include directory. Version/source/hash/edit ambiguity fails configuration.
The registry currently has exactly four entries:
| Entry | Component | Protected original SHA-256 |
| --- | --- | --- |
| `wolfssh_internal` | `wolfssl__wolfssh` | `81ff1f9166708abd5c2911e9fe57c0aee01c88b5d3f68c909ee8a856d37f36a9` |
| `https_server` | `esp_https_server` | `6481942b62e51125e2a43441fa0900cbda74bd2ea05c82f0c29eb4933c31946e` |
| `httpd_parse` | `esp_http_server` | `6bba77064aaa68a06f8d4c01432064a1b050c91ed22741c547785b0d8a6c07d8` |
| `esp_tls_mbedtls` | `esp-tls` | `09210c5a601647ca5775d127a2951bab2f3e509192b53487bbea8a93d8731b78` |
Read-only Python inspection called `verify_version()` and `render_entry()` in memory (not `generate()`), verified all four original pins, and compared rendered bytes with the existing generated files: **all four matched exactly**. This did not rewrite installed or generated files.
The existing `.pio/build/esp32-s3-devkitc-1-n16r8/security_overrides/manifest.cmake` and narrowly filtered `compile_commands.json` register those four generated sources, but register **original** `dhcpserver.c`, `httpd_ws.c`, `ssl_tls.c`, `asn1write.c`, and `x509_create.c`. Thus HTTPS cleanup, HTTPD scratch wiping/null handling, TLS policy, and wolfSSH fixes do **not** constitute fixes for these five findings.
This confirms existing configured compilation inputs, not freshness of the executable, successful rebuilding, or the firmware installed on a device.
### mbedTLS registration constraint
Installed `components/mbedtls/CMakeLists.txt` imports upstream nested targets. The existing compile database's object paths confirm:
- `ssl_tls.c` → target `mbedtls` (`.../CMakeFiles/mbedtls.dir/ssl_tls.c.obj`).
- `x509_create.c` → target `mbedx509`.
- `asn1write.c` → target `mbedcrypto`.
The current replacement helper resolves an **IDF component's** `COMPONENT_LIB`; it does not resolve these nested library targets. Simply adding `Entry(..., "mbedtls", ...)` would select the component wrapper, not the actual owning target, and should fail its exactly-one-source check. A future patch must minimally add an explicit, validated target-selection mechanism for these three nested targets while retaining current component resolution for existing entries. Do not weaken the match check or add duplicate source compilation to the wrapper.
## CVE-2026-45160 — DHCP server `parse_options()`
Official [GHSA-g764-gwc3-75m5](https://github.com/espressif/esp-idf/security/advisories/GHSA-g764-gwc3-75m5) and fetched [release/v5.5 patch](https://github.com/espressif/esp-idf/commit/d51b1076092487e533eadf8b48c9c8579d3a6712.patch).
**Exact upstream fix commit:** `d51b1076092487e533eadf8b48c9c8579d3a6712`.
Confirmed installed `dhcpserver.c:902952`:
- Loop only checks `optptr < end`.
- Message type reads `optptr[2]` without validating a length byte or payload.
- Requested IP compares four bytes without a declared/remaining-length check.
- Advancement reads `optptr[1]` without ensuring it exists.
- PAD is not treated as a one-byte option.
The GHSA lists later affected point releases and a planned 5.5.5 fix; that is **not evidence excluding 5.5.0**. The old implementation is directly present here.
### Reachability
`src/wifi_manager.c:create_default_wifi_netifs()` explicitly builds `ESP_NETIF_DEFAULT_WIFI_AP()` and installs default AP handlers. Installed `esp_netif_defaults.h:5961` sets `ESP_NETIF_DHCP_SERVER | ESP_NETIF_FLAG_AUTOUP`. Existing generated configuration has `CONFIG_LWIP_DHCPS=1`. `start_radio_and_policy()` selects AP/APSTA when policy requires it; `set_runtime_ap_enabled()` also permits STA→APSTA activation.
This is therefore an applicable exposure when SoftAP is active, not a hypothetical unused SDK feature. A station-only moment is not a permanent mitigation if fallback or administrative policy can enable AP later. The official impact is adjacent-network denial of service, not demonstrated heap disclosure: the advisory says overread bytes are not echoed. WPA association controls reduce access but do not fix the parser. No live attack or actual device AP state was tested.
### Minimal recommended edits (not implemented)
Add a pinned `idf` entry for `components/lwip/apps/dhcpserver/dhcpserver.c`, component `lwip`, using the exact original hash above. Backport the official patch's PAD definition and parser changes:
1. Skip PAD; terminate at END before reading a length.
2. Require a length byte and the entire declared payload to fit.
3. Require message-type payload length ≥1 and requested-IP length ≥4.
4. Advance using the already-validated `opt_len`; remove the obsolete end flag.
5. Retain notices and existing post-loop state behavior.
For a strict-C bounds implementation, consider remaining-length comparisons (`end - optptr >= 2`, then `opt_len <= remaining - 2`) rather than constructing a pointer beyond the object in the upstream `optptr + 2 + opt_len > end` check. If chosen, document this small deviation and test equivalence; this review did not apply or test either form.
Future tests: empty options, lone option code, missing length, oversized payload, PAD sequences, END at boundary, short type/IP fields, valid DISCOVER/REQUEST/RELEASE and option combinations. Verify actual generated `lwip` compilation input, no duplicate original, and no changes to AP policy, broker, UART0, or USB recovery.
## CVE-2026-45541 — WS subprotocol negotiation
Official [GHSA-3j8v-xgrq-5vg8](https://github.com/espressif/esp-idf/security/advisories/GHSA-3j8v-xgrq-5vg8) and fetched [release/v5.5 patch](https://github.com/espressif/esp-idf/commit/00a2f7fbbbd8fe6d04729022e1d5c9a49435bfe8.patch).
**Exact upstream fix commit:** `00a2f7fbbbd8fe6d04729022e1d5c9a49435bfe8`.
Installed `httpd_ws.c:7095` has `strtok_r()` followed by a `do` loop that dereferences its possibly NULL result. It also compares using `sizeof(subprotocol)`, the pointer size rather than protocol length. A nonempty delimiter-only header can yield no tokens.
### Current route registration avoids the defective loop
- Existing configuration has `CONFIG_HTTPD_WS_SUPPORT=1`; disabling WS at compile time is **not** the reason for non-reachability.
- `src/web_server.c:518539` defines serial and admin upgrade routes with `.is_websocket = false`; omitted `supported_subprotocol` members of these static initializers are NULL. `start_server()` actually registers the serial handler array and optional admin route.
- `src/web_serial_transport.c:745` and `src/web_admin_transport.c:570` use `web_httpd_upgrade()` after admission.
- `src/web_httpd_adapter.c:174` calls `httpd_ws_respond_server_handshake(request, NULL)`.
- Installed `httpd_ws_get_response_subprotocol()` returns at its `supported_subprotocol == NULL` check **before** tokenization. The optional registration adapter also rejects `is_websocket` or `supported_subprotocol`.
Conclusion: vulnerable library bytes are present, but **this CVE's NULL-token dereference is not reachable through the current application upgrade paths**. The official generic warning about pre-application-authentication crashes applies to automatic SDK WS negotiation with a non-NULL supported protocol; it should not be copied onto these manual-admission routes without this qualification. TLS alone is not a general mitigation.
### Minimal recommended edits (not implemented)
Add a separately pinned `httpd_ws.c` entry for component `esp_http_server`; do not conflate it with the existing `httpd_parse.c` entry. Backport the upstream `while (s != NULL)` loop, exact-length match and explicit next-token step, retaining notices. Do not enable automatic upgrades or add a subprotocol to justify the patch.
Future tests: empty/delimiter-only headers, NULL supported protocol, unsupported protocol, exact match and prefix mismatch, plus current cookie/Origin/ticket admission-before-101 behavior. Mark it as a defense-in-depth fix, not closure of a demonstrated unauthenticated application crash.
## Bounded mbedTLS 3.6.3 review
### CVE-2026-50581 — EMS calculation failure ignored
Official [advisory](https://raw.githubusercontent.com/Mbed-TLS/mbedtls-docs/main/security-advisories/mbedtls-security-advisory-2026-07-extended-master-secret-calculation-failure-ignored.md), published 2026-07-07. Affects through 3.6.6; first fixed 3.6.x release is **3.6.7**, not 3.6.6.
Fetched [basic fix](https://github.com/Mbed-TLS/mbedtls/commit/f595df4569c1a1650ad9d077e2f2e819e9f1dddb.patch): **`f595df4569c1a1650ad9d077e2f2e819e9f1dddb`**. Advisory gives test/documentation range `338572c1d805a31b875a448536bcb50d72f9bc40..27065ceb643a4266888ba1f200e4f20845e801cf`; that range was not reviewed here.
Confirmed installed `ssl_tls.c:70417085`: `session_hash[48]`, initial `seed_len=64`, and `handshake->calc_verify()` error only logged; execution continues. The override edits `esp-tls/esp_tls_mbedtls.c`, **not** this file. The server's two ECDHE-ECDSA AES-GCM suites use SHA-256/SHA-384 and TLS 1.2; EMS remains relevant even without renegotiation. Installed `esp_config.h:10941097` enables EMS when `CONFIG_MBEDTLS_TLS_ENABLED`, which is 1 in the existing generated header.
A calculation/allocation failure can leave an uninitialized hash and potentially retain length 64, causing a 16-byte buffer overread. Per upstream, this does not directly disclose stack contents or the master secret; a correctly functioning peer rejects the incorrect Finished message. Higher-level master-secret uniqueness attacks require additional conditions, including both peers affected and induced failures. **No direct HTTPS login bypass, practical remote fault trigger, or device exploit was established.** Low historical memory minima justify fault testing, not an assertion that this failure occurred.
**Minimal recommendation:** pinned `ssl_tls.c` replacement on actual target `mbedtls`, inserting `return ret;` immediately after the existing `MBEDTLS_SSL_DEBUG_RET(1, "calc_verify", ret)` inside the error branch. Preserve EMS; do not disable it as a shortcut. The fetched commit also changes `ssl_tls13_server.c` for a separate resumption-secret error: do not claim the one-line TLS 1.2 backport implements that second fix or globally updates mbedTLS. Current HTTPS policy is TLS 1.2 only.
Future tests should inject `calc_verify` failure for both SHA paths, ensure PRF/key derivation and successful handshake progression do not occur, preserve success behavior, and exercise cleanup. Exact driver/backend failure inducibility on the target remains unresolved; no preprocessing/cryptographic or fault-injection test was run.
### CVE-2025-48965 — ASN.1 named-data inconsistency, not OOM
Official [advisory](https://raw.githubusercontent.com/Mbed-TLS/mbedtls-docs/main/security-advisories/mbedtls-security-advisory-2025-06-6.md) and [3.6.4 release notes](https://github.com/Mbed-TLS/mbedtls/releases/tag/mbedtls-3.6.4). Affects through 3.6.3; fixed in 3.6.4.
Installed `asn1write.c:374435` checks allocation failures when creating a node/OID/value and preserves old data until replacement allocation succeeds. However, the `val_len == 0` branch frees `cur->val.p` and sets it NULL without resetting `cur->val.len`. Reusing the same OID or writing the inconsistent list can dereference NULL. Official examples include `DC=foo,DC=#0000` and `DC=foo,DC=#0000,DC=bar`.
`src/web_security.c:92111,128257` builds `CN=ESP32 SAK <six hex digits>` from the MAC, initializes a fresh writer, and sets subject and issuer separately. There is no repeated OID or empty value in either name. Thus the faulty primitive exists and is indirectly called, but **the official hostile-name trigger is not supplied by this certificate-generation path**. Do not say “not applicable because the project never creates certificates”; it does. Do not treat separate subject/issuer lists as repeated attributes in one list.
**Minimal recommendation:** ensure `cur->val.len = 0` when clearing the value, on actual target `mbedcrypto`, with a pin on the installed file. Retain the existing preserve-old-on-allocation-failure logic. The official advisory/release was fetched, but **an exact upstream CVE-2025-48965 fix commit was not resolved or patch-verified in this bounded review**; obtain and compare the specific 3.6.4 change before implementing/pinning a claimed official backport. Test nonempty→empty→same-length replacement, list serialization after empty replacement, repeated OIDs, and allocation failures independently.
### CVE-2026-34874 — `x509_string_to_names()` allocation failure
Official [advisory](https://raw.githubusercontent.com/Mbed-TLS/mbedtls-docs/main/security-advisories/mbedtls-security-advisory-2026-03-null-pointer-dereference-x509.md) and [3.6.6 release notes](https://github.com/Mbed-TLS/mbedtls/releases/tag/mbedtls-3.6.6). Affects 3.5.03.6.5; fixed in 3.6.6.
Fetched [basic fix](https://github.com/Mbed-TLS/mbedtls/commit/bfaf4a47fd33da860796feaba6235847acb71127.patch): **`bfaf4a47fd33da860796feaba6235847acb71127`**. The advisory identifies tests/documentation through `4704b6b4bd963f1331582374e881184addf8f523`; those additional changes were not reviewed here.
Installed `x509_create.c:306310` allocates the known attribute's OID and calls `memcpy(oid.p, ...)` without checking NULL. The project's CN takes exactly this recognized-attribute branch through both name setters. Trusted names avoid the previous CVE, **not this OOM defect**. Application return-code checks cannot catch a fault that happens inside the setter before it returns.
**Minimal recommendation:** pinned `x509_create.c` replacement on actual target `mbedx509`; directly after `oid.p = mbedtls_calloc(1, oid.len)`, add the official three-line NULL guard returning `MBEDTLS_ERR_X509_ALLOC_FAILED`. Preserve existing application cleanup and fail-closed identity publication. No new buffer or policy change is required.
The upstream advisory discusses arbitrary code execution on microcontrollers where address zero is writable. **That impact was not demonstrated for this ESP32-S3 memory map**; confirmed here is the unchecked write path and potential failure during certificate generation, not practical code execution. Test OID allocation failure at each setter, clean writer destruction, no persistence/publication of incomplete material, preservation of the old identity on failed rotation, and successful retry.
## Actionable next correction and remaining evidence
1. **Implement the pinned DHCP parser backport first.** It is the clearest currently reachable unauthenticated network-input defect in this scope. Do not merely change advisory wording or a version label.
2. In the same security correction effort, support explicit nested-target overrides and backport the EMS early return and X.509 allocation guard. Verify each protected source appears exactly once on its real owner target; retain all existing four corrections and their source properties.
3. Backport WS defensively and add registration/NULL-subprotocol regression coverage without changing application admission. Resolve the exact ASN.1 3.6.4 patch before adding that lower-priority correction.
4. Before acceptance, run focused parser/allocation/error-propagation tests against the generated compilation inputs, then a firmware build and target checks under separately authorized implementation/validation work. **None were performed for this review.** Ensure failures cannot compromise UART0 recovery, native USB independence, broker ownership, or bounded transport isolation.
5. A full IDF upgrade is an alternative maintenance strategy, but must rebase/re-audit all existing source pins, private HTTPD contracts, and TLS behavior. Updating only to mbedTLS 3.6.6 would leave CVE-2026-50581 unresolved. This document does not certify any upgrade candidate against the full advisory set.
Unresolved boundaries: exact ASN.1 fix commit; target-specific OOM/hash failure reproducibility and impact; actual running firmware/configuration; complete mbedTLS/IDF advisory coverage and downstream release integration. Existing source registration and generated-byte equality are confirmed; patch application, tests, linking, and device behavior are not.
Review mechanics: project code-map/current-state read first; targeted source, configuration and compilation-input inspection; official advisories and four exact fix patches fetched. No large release-history index was used. Some GitHub API requests timed out/returned no results and direct ReadTheDocs access via Python returned HTTP 403; official raw advisory sources and individual release pages supplied the evidence above. One read-only inspection command hit its 10-second timeout; a linear-time replacement completed the inspection. No source modifications were made, and this review owns only this document.
+1 -1
View File
@@ -216,7 +216,7 @@ Staged work:
1. **9A — Crash/debug build policy and operational profiles — In progress; hardware pending.** `src/security_build_policy.c` requires `CONFIG_ESP_COREDUMP_ENABLE_TO_NONE=y` and `CONFIG_ESP_SYSTEM_PANIC_SILENT_REBOOT=y`; rejects core-dump enable/flash/UART, panic print/halt/GDBstub, runtime GDBstub and ESP/FreeRTOS debugger-aware options. `sdkconfig.defaults` makes the baseline explicit. Development/test/production use the same build baseline, not separate PlatformIO environments. Host matrix (`python3 tests/security_build_policy/run.py`) compiles the actual guard: 17 cases plus the generated-header check passed on 2026-09-15. `pio run` passed (94,196 B linked RAM / 1,828,565 B flash); target panic/recovery tests have not run. Production readiness remains pending.
2. **9B — SSH admission and credential handling — Implemented; combined target validation deferred.** Boot-lifetime, owner-only token buckets independently bound handshakes, password/signed-key requests and unsigned probes; reconnect/restart/counter clearing do not replenish them. Existing per-slot attempt limits/currentness remain. Explicit keyboard-interactive rejection, pending-signature result fencing, secret-free admission counters, consumed admin-buffer wipes and fail-closed hidden-prompt overflow/unsupported-byte handling are implemented. Four focused suites (including 35 pinned-vendor control-flow cases), 11 related regressions and `pio run` passed on 2026-09-15: 94,340 B linked RAM / 1,829,925 B flash. Global-budget starvation remains a documented tradeoff, not a solved availability problem.
3. **9C — Library cleanup and protocol policy — Implemented; combined target validation deferred.** Exact-hash build-tree overrides correct HTTPS cleanup/leaks, HTTPD scratch failure/wiping/first-read handling, bounded SSH password parsing/wiping and server-local TLS policy without modifying installed dependencies. Secure wolfSSL allocation hooks and explicit SSH policy fail closed; early web quota probing avoids receiving already-throttled bodies. TLS1.2 ECDHE-ECDSA AES-GCM and SSH GCM/modern-KEX allowlists intentionally exclude legacy-only clients; no identity migration. Bounded password/certificate/header/destructor review is documented, not exhaustive zeroization. Final build PASS 94,340 B linked RAM / 1,831,309 B flash; focused and related host/source-contract tests passed. [Review and maintenance contract](security_library_review.md).
4. **Next — 9D maintenance and lifecycle.** Review current dependency advisories and licenses without assuming pinned versions or local corrections are permanently safe; document provisioning, rotation, factory reset, backup, recovery and decommissioning. Upgrades require re-auditing the source overrides. OTA signing trust needs an independent policy without secure boot (Phase 10).
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. Server-recommended message-order work (CVE-2025-14942) and parser/ECC reviews remain pending. 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,767,217 B flash**, unchanged RAM / **64,092 B flash** from 9C; SDK-override, wolf-crypto-policy, wolfSSH-auth-contract and SSH-protocol-policy suites all passed. Independent reviewer reports the first two suites passed with no blocking implementation defects; see [evidence and limits](security_hardening.md#mitigation-hostbuild-evidence--2026-09-15). The [bounded license inventory](dependency_licenses.md) marks modification/date notices resolved for all seven generated files (2026-09-15; upstream licenses retained), but radio-blob corresponding-source/exception, source/notice delivery and packaging/provenance questions remain open. 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.
+31 -6
View File
@@ -1,6 +1,6 @@
# Security hardening — Phase 9
**Status: in progress.** Phase 8 is complete at the accepted 8D.22 scope. **9A crash/debug policy, 9B SSH admission/credential handling and 9C library cleanup/protocol policy** are implemented with host/build validation. At the user's request, hardware validation is deferred to **Phase 9 as a whole**, not an approval gate between implementation slices. This document records policy and procedures, not unrun passes or production certification.
**Status: in progress.** Phase 8 is complete at the accepted 8D.22 scope. **9A crash/debug policy, 9B SSH admission/credential handling and 9C library cleanup/protocol policy** are implemented with host/build validation. At the user's request, hardware validation is deferred to **Phase 9 as a whole**, not an approval gate between implementation slices. **9D maintenance/lifecycle is in progress, with unresolved advisory and distribution/source/notice questions. Phase 9 is not complete or production-ready.** This document records policy and procedures, not unrun passes or production certification.
## Scope and threat model
@@ -71,7 +71,7 @@ Hidden UART0 and shared remote-console prompts now reject overflow or unsupporte
### Reproducible source corrections
`tools/security_overrides.py` verifies full original-file SHA-256 values and ESP-IDF 5.5.0, applies exact-once edits, and generates four corrected sources under the build directory. `cmake/security_overrides.cmake`, included after `project()`, replaces exactly the corresponding sources in existing IDF/component targets, retaining compilation properties. Installed SDK/managed sources and their notices remain unchanged. Missing, changed or ambiguous sources fail configuration; there is no unpatched fallback. Do not edit derived files or repin a hash merely to make an upgrade build.
`tools/security_overrides.py` verifies full original-file SHA-256 values and ESP-IDF 5.5.0, applies exact-once edits, and generates seven corrected sources under the build directory (four from 9C plus three advisory backports in 9D). `cmake/security_overrides.cmake`, included after `project()`, replaces exactly the corresponding sources in existing IDF/component targets, retaining compilation properties. Installed SDK/managed sources and their notices remain unchanged. Missing, changed or ambiguous sources fail configuration; there is no unpatched fallback. Do not edit derived files or repin a hash merely to make an upgrade build.
- **HTTPS:** delete TLS on post-handshake transport-allocation failure; fully destroy retained TLS configuration on failed HTTPD start; wipe the copied raw private key before free. Failed stop still retains live ownership.
- **HTTPD parser:** allocate/copy/wipe/free scratch on resize, preserve the old pointer on allocation failure, wipe final scratch, and handle the null initial parser pointer without undefined subtraction. Pending/unread bytes retain their existing behavior.
@@ -102,6 +102,16 @@ A non-consuming quota/epoch check now runs after valid challenge consumption but
The existing global five-verifications/60-second fixed window is unchanged: malformed requests do not charge it, counter clear does not replenish it, and web service restart does. This differs deliberately from SSH's boot-lifetime buckets. The first boot-minute window remains anchored at uptime zero. Challenge monopolization, global-budget starvation and malformed-body work while budget is available are not solved by this early check.
## 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. **Still open:** server-recommended message-order correction for **CVE-2025-14942**, parser/ECC review and broader API/feature applicability. No server credential-leak or invalid-curve exploit is demonstrated; target timing/interoperability remains untested.
- [Focused IDF review and implementation addendum](idf_security_review.md): pinned backports now implement DHCP option bounds (**CVE-2026-45160**), TLS 1.2 EMS failure return (**CVE-2026-50581**) and X.509 OID allocation-failure handling (**CVE-2026-34874**). Explicit nested-target validation places the mbedTLS edits on `mbedtls`/`mbedx509`, retaining source properties and exactly-one-source checks. WS negotiation **CVE-2026-45541** and ASN.1 named-data **CVE-2025-48965** remain unpatched with the review's qualified applicability, not blanket closure.
- [Dependency license inventory](dependency_licenses.md) is a bounded engineering inventory, not legal or distribution clearance. All seven generated sources now carry prominent modification notices dated **2026-09-15**, with upstream notices retained, including both mbedTLS dual-license headers. That narrow finding is resolved. Radio-blob corresponding-source/exception questions, actual firmware/device/browser notice delivery, preferred-source packaging, wolfSSH package-license discrepancy and icon provenance/full-license work remain open. Existing repository notices alone are not a complete release bundle.
- No dependency versions were upgraded. Beyond the implemented backports above, proposed upgrades/backports remain candidates, not approved compatible versions. Re-audit coherent source/header changes, effective compile policy, exact-hash overrides and callback/parser contracts, then obtain host/build and whole-phase target evidence. 9A9C passes below are historical scoped evidence, not closure of these newly recorded findings.
Secure boot and encrypted NVS remain excluded. No runbook, advisory report or license inventory establishes production readiness or authorizes a destructive device operation.
## Operational profiles
These are handling and validation profiles of the **same supported build baseline**, not separate PlatformIO environments or selectable security overrides.
@@ -118,7 +128,22 @@ Raw flash, RAM and dumps can contain Wi-Fi passwords, private keys, password ver
## Validation gates
### Host and build — passed 2026-09-15 (9A9C)
### 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,767,217 B flash**. Against the historical 9C build below, RAM is unchanged and flash is **64,092 B smaller**. Linked size is not runtime headroom or timing evidence.
All four parent commands passed:
```sh
CCACHE_DISABLE=1 python3 tests/sdk_security_overrides/run.py --build-dir .pio/build/esp32-s3-devkitc-1-n16r8
CCACHE_DISABLE=1 python3 tests/wolf_crypto_policy/run.py
CCACHE_DISABLE=1 python3 tests/wolfssh_auth_contract/run.py
CCACHE_DISABLE=1 python3 tests/ssh_protocol_policy/run.py
```
The independent reviewer reports the first two suites passed with no blocking implementation defects. Documentation verification separately matched all seven existing generated files to pinned originals plus checked-in edits/notices and checked retained mbedTLS license headers. This is scoped implementation/host/build evidence, not completed wolfSSH ordering/parser/ECC review, license packaging clearance or whole-phase acceptance. No target evidence, upgrade, asset regeneration or device operation is claimed.
### Host and build — historical passes 2026-09-15 (9A9C)
From the repository root:
@@ -136,13 +161,13 @@ python3 tests/ssh_protocol_policy/run.py
python3 tests/web_cookie_auth/run.py --admission
```
Latest 9C `pio run` passed with **94,340 B linked RAM / 1,831,309 B flash**, unchanged linked RAM / +1,384 B flash against 9B. This is linked size, not measured runtime headroom. Five focused suites passed after final HTTPD first-read correction: pinned SDK cleanup/TLS/source registration, 135 generated wolfSSH parser/control-flow cases, secure allocator, SSH policy (including 15 actual context-integration cases), and web early admission/wiping. The allocator's optional installed-SDK extent contract was also run with the installed IDF path and passed; plain invocation reports that optional check skipped. Seventeen integrated regression commands passed before the final first-read addition, including SSH auth/management, HTTPD idle, HTTPS lifecycle, five cookie-auth modes and the 18-case crash-policy matrix. All ten existing cookie-auth domain modes also passed during implementation. Independent reviews found no blocking issues; the identified inherited first-read pointer issue was corrected and tested. Use `CCACHE_DISABLE=1` on host commands if the compiler wrapper's cache is read-only in a sandbox. No upload, erase, eFuse operation or target test was performed.
Historical final 9C `pio run` passed with **94,340 B linked RAM / 1,831,309 B flash**, unchanged linked RAM / +1,384 B flash against 9B. This is linked size, not measured runtime headroom. Five focused suites passed after final HTTPD first-read correction: pinned SDK cleanup/TLS/source registration, 135 generated wolfSSH parser/control-flow cases, secure allocator, SSH policy (including 15 actual context-integration cases), and web early admission/wiping. The allocator's optional installed-SDK extent contract was also run with the installed IDF path and passed; plain invocation reports that optional check skipped. Seventeen integrated regression commands passed before the final first-read addition, including SSH auth/management, HTTPD idle, HTTPS lifecycle, five cookie-auth modes and the 18-case crash-policy matrix. All ten existing cookie-auth domain modes also passed during implementation. Independent reviews found no blocking issues; the identified inherited first-read pointer issue was corrected and tested. Use `CCACHE_DISABLE=1` on host commands if the compiler wrapper's cache is read-only in a sandbox. No upload, erase, eFuse operation or target test was performed.
Record the revision, compiler/build outcome and effective configuration. Confirm that the matrix accepts the supported configuration, rejects each prohibited option independently, and rejects absent/disabled required settings. Confirm the normal firmware build compiles the guard. A rejected unsafe configuration is an expected negative-test result, not a firmware build pass. Neither these commands nor a successful build proves target panic behavior.
### Combined Phase 9 target validation — deferred, not run
Retain these checks for the final phase test session; do not stop implementation for a separate 9A/9B/9C sign-off.
Retain these checks for the user's final whole-phase test session; do not stop implementation for separate slice sign-off. Include the [9D operational rehearsal](security_operations.md#deferred-whole-phase-9-hardware-rehearsal) and targeted message-order/parser/key-validation/interoperability checks for the eventual reviewed advisory mitigations. None is recorded as passed here.
#### Crash and recovery
@@ -171,7 +196,7 @@ Retain these checks for the final phase test session; do not stop implementation
## Staged next work
- **Next — 9D maintenance and lifecycle.** Review current ESP-IDF/wolfSSL/wolfSSH advisories and third-party licenses, then plan any upgrades and source re-audits separately. Complete provisioning, key rotation, backup, factory reset, recovery and decommissioning runbooks without claiming physical-extraction resistance or secure erasure. No external advisory or complete license review has been performed by 9C.
- **Continue 9D maintenance and lifecycle.** Resolve the [open advisory priorities](wolf_security_review.md#release-decision--current-path-priorities), 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.
+117
View File
@@ -0,0 +1,117 @@
# Security operations — Phase 9D
Operator runbook for the current firmware; procedures below are **not execution records**.
Use the [command reference](command_reference.md) for complete syntax and the [browser contracts](web_administration.md) for frontend permissions and result handling.
No device operation is authorized merely by this document. Schedule disruptive work, retain trusted UART0 access, and obtain explicit approval for destructive recovery/erase.
The user will validate **whole Phase 9**, not provide per-segment sign-off. **9D is in progress; Phase 9 is not complete or production-ready.** The [partial wolfSSL/wolfSSH advisory review](wolf_security_review.md) leaves current-path security findings open; the [bounded license inventory](dependency_licenses.md) leaves distribution/corresponding-source/notice questions unresolved. This runbook neither resolves those findings nor claims device validation or license clearance.
## Safety and access
- Restrict physical access: UART0 is the trusted administrative recovery authority. Use the USB-to-UART bridge at 115200 baud, not native USB CDC.
- Native USB is network-independent, binary-transparent UART1 access through the one-writer/multiple-observer broker; it is not an admin console. Reboot interrupts every transport.
- Use a trusted workstation and restricted provisioning network. Disable terminal recording, screen sharing and clipboard history before secret entry/reveal; do not put secrets in command arguments, tickets, screenshots or routine logs.
- UART0 and authenticated role-`admin` SSH provide the operational commands below, except `user recover --force` is UART0-only. Role-`user` SSH is serial, not administration.
- Browser shell is narrower: other-account interactive add/password and forced role/delete only; no self/generated/key/recovery account commands. Wi-Fi/mDNS shell access is status-only. Typed Settings has separate account/network/identity workflows; do not infer shell parity.
- NVS is **unencrypted**: Wi-Fi PSKs and HTTPS/SSH private keys are plaintext at rest; password verifiers are also sensitive. Secure boot and NVS encryption are not provided by this phase. Physical flash extraction and firmware replacement remain outside the protection boundary.
## First provisioning — UART0 first
1. Start in a controlled RF environment with UART0 attached. Defaults enable Wi-Fi with a random-password fallback AP and no station profiles; networking can start before the console is ready. A new AP name is not an authentication secret.
2. Once the console is ready, use `wifi stop`, inspect `wifi status`, then `wifi save` if Wi-Fi must remain disabled after reboot. Stop is asynchronous: check actual state, not only the queue acknowledgement. Do not rely on this to prevent the initial boot's radio exposure.
3. Inspect `user status`. Missing user storage initializes empty; create the first administrator with `user add <username> admin` on UART0 and the hidden confirmation prompts. There is no bootstrap/shared default web credential. A healthy empty database needs **add**, not recovery.
4. Store the chosen password in the operator's protected vault. Usernames and password bounds are in the command reference (passwords: 1264 printable ASCII characters). Optionally use `--generate` only with private, reliable one-time capture. Create/test a separately controlled recovery administrator if policy requires one; the final administrator cannot be deleted or demoted.
5. Choose AP policy explicitly with `wifi ap policy off` or `wifi ap policy fallback` (use `always` only intentionally). For retained AP access, set `wifi ap secret` through the hidden 863-character prompt, or privately retrieve the generated password with `wifi ap show-secret`. Never include its output in routine captures.
6. Configure an unused/disabled station slot using `wifi profile set <slot> <priority> <mixed|wpa3> <ssid>`, then `wifi profile secret <slot>`, then `wifi profile enable <slot>`. Check peer compatibility before choosing WPA3. Profile set preserves the existing secret/enabled state; disable an existing slot before staging its replacement. Enabling or editing an enabled profile/AP can reconnect the radio.
7. Use `wifi start` when ready, inspect `wifi status` / `wifi profiles`, and `wifi save` to persist the intended working policy, profiles, secrets and boot-enabled state. Restrict upstream network access to intended operators; do not expose ports 443/22 to untrusted networks.
8. Read `web certificate info` and `ssh host-key info` through trusted UART0. Compare the certificate SHA-256 fingerprint in the HTTPS client and the OpenSSH `SHA256:` host-key fingerprint **before supplying account credentials or accepting changed trust**. A self-signed certificate or an unsolicited network fingerprint alone is not proof of device identity.
9. Establish narrowly scoped browser certificate trust and SSH known-host trust only after comparison. Investigate an unexpected mismatch rather than disabling verification or deleting all known-host entries. Test fresh HTTPS login and both intended SSH roles; an admin SSH login does not acquire a serial writer lease.
10. Set required serial framing, mDNS suffix and display policy with their documented commands and explicit saves. Record intended state privately; later rehearse a reboot and compare actual loaded settings/trust before production use.
**Fallback-default caution:** true first boot attempts to persist generated Wi-Fi defaults, but that save can fail. Incompatible stored configuration selects generated RAM defaults without overwriting the rejected blob. `wifi defaults` generates fresh RAM defaults; `wifi reset` generates and saves them. Both restore enabled/fallback policy and replace the AP password, not merely repair connectivity. Re-protect the AP and verify/save intended state rather than assuming an old password or disabled radio survived.
## Working configuration versus service lifecycle
| Operation/domain | Persistence and operational effect |
|---|---|
| Serial, Wi-Fi, mDNS, display edits | Working RAM until `serial save`, `wifi save`, `mdns save` or `display save`, respectively; `load` reads that domain's storage, not an external backup. `defaults` is RAM; `reset` applies/persists domain defaults. Check errors and actual state; these domains do not share one atomic transaction. |
| `wifi start` / `wifi stop` | Queue runtime action **and** set RAM `enabled-at-boot` true/false; only `wifi save` persists it. Local start/stop has the same semantics. |
| `web start` / `web stop`, `ssh start` / `ssh stop` | Runtime service controls, not persistent enable/disable configuration. No corresponding service `save` or durable boot-disable command. Stops disconnect that service's clients. |
| Boot network services | `main.c` attempts HTTPS/SSH startup independently when Wi-Fi initialization/start has no error and each service's security/runtime initialization succeeds. Wi-Fi configured off can still initialize successfully: listeners may be started without radio reachability. Wi-Fi-off is not a persistent HTTPS/SSH-disable flag. |
| Accounts and identities | Mutations commit their own NVS blob as part of the operation; no follow-up `save`. Commit acknowledgement is not a general power-loss/durability guarantee. |
| `reboot` | Loses unsaved RAM and all live sessions. A runtime service stop does not survive normal boot startup. |
## Routine account, password and authorized-key rotation
1. Keep UART0 or a separate current administrator available. Inspect `user show <username>` privately to confirm role and current key slots; do not operate from stale indices. The database allows eight accounts and three authorized keys per account.
2. Set a known replacement with `user password <username>` using hidden prompts. `user password <username> --generate` commits then displays a value once: protect it immediately. Lost generated output has no retrieval command; inspect account state, then deliberately set another known password via UART0/another admin rather than blindly replaying generation.
3. Admin SSH **rejects its own generated-password replacement**; its ordinary hidden self-password change is allowed but revokes the invoking principal. Browser shell denies all self account mutations. Typed Accounts supports self changes; its Generate action only returns a value, does not change the account, and has no retained retrieval. Securely save/acknowledge it before separately submitting the mutation.
4. For an authorized-key rotation, generate/protect the private key on the operator's host; import only the supported Ed25519 or ECDSA-P256 **public** key with `user key add <username>`. With capacity available, add first, test a fresh login using the new key, then remove the old key using its freshly inspected slot: `user key delete <username> <0..2> --force`. If full, use independent recovery access before removing a slot. `user key clear <username> --force` removes all that account's keys, not its password.
5. Password, role and key changes stale the affected principal; deletion removes the account. Committed changes trigger targeted web-ticket/session and SSH revocation, supplemented by authoritative currentness checks. Self changes can lose their result before it arrives; unrelated accounts should remain usable. Disconnect/timeout is not proof of rollback or cancellation of admitted work.
6. Reauthenticate to verify the new credential and rejection of the retired one; do not confuse admission throttling with a bad password. For account retirement use `user delete <username> --force`; for reduced privilege use `user role <username> user --force`. Preserve a usable administrator. A password change does **not** remove authorized keys, and key removal does **not** change the password: revoke both paths for a compromised account.
7. Update the protected operator record and revoke any reused credentials/keys on other systems separately. A key can authorize multiple device accounts; remove every affected authorization. Retire superseded host-side private-key copies according to storage policy.
## HTTPS and SSH server identity rotation
Plan a maintenance window and distribute newly verified public trust through a trusted channel. Server identity changes are separate from account passwords/authorized keys and do not replace them.
- **HTTPS:** `web certificate rotate --force` replaces certificate **and** private key. Owner ordering is reserve → generate/commit/publish → stop/restart if running. Precommit failure leaves the old identity/logins in place; after commit, lifecycle failure never rolls the identity back. Failed stop can leave the old certificate served while UART0 reports new stored material. Rotation while stopped stays stopped.
- **SSH:** `ssh host-key rotate --force` reserves ownership → stops sessions → generates/commits/publishes → restarts if previously running. Failed stop skips mutation/start. Persistence failure may follow client disconnection and attempts to restart with unchanged material; that restart can also fail. A committed new key is never rolled back after restart failure. Rotation while stopped stays stopped.
- UART0/admin SSH expose both rotations. Browser shell allows only exact `web certificate rotate --force`, not SSH host-key mutation; typed HTTPS/SSH Settings provides its own bounded rotation flow. Neither browser route provides identity reset/recovery/export.
- Remote scheduling/drain/HTTP acknowledgement is not peer receipt or operation completion. On timeout/lost result, use Check Result/Refresh where available, then inspect UART0 service status and fingerprints. **Do not automatically repeat rotation.**
- Resolve lifecycle failures with deliberate UART0 `web stop` / `web start` or `ssh stop` / `ssh start`, checking each outcome; do not start over failed cleanup or rotate again to repair it. If recovery needs reboot, account for unsaved state and all-transport interruption.
- After recovery, compare the actually served identity against trusted UART0, replace only the relevant client trust entry, and test a fresh connection. HTTPS restart requires fresh login; SSH rotation closes SSH sessions but does not inherently require HTTPS relogin. USB/UART0 remain independent of these network-service rotations, not of reboot.
## Configuration backup and reconstruction
There is **no implemented configuration export/import or full backup/restore workflow**, nor a supported private-identity export/restore command. `save` writes working state to the same device; it is not an off-device backup. Do not use raw NVS/flash dumps as routine backups or duplicate them onto replacement devices.
Maintain an access-controlled operator inventory outside the repository: reviewed firmware/build provenance, intended serial settings, station/AP policy and profile ordering, boot-enabled intent, mDNS/display settings, account roles, authorized-public-key provenance, verified public server fingerprints, and last successful save/rehearsal outcomes. Keep device/account/network identifiers in that protected record, not public issue logs. This runbook intentionally contains no populated inventory or identifier template.
Keep passwords, Wi-Fi PSKs and client private keys in a separately protected, backed-up vault with controlled recovery access and retention. Inventory entries should reference vault records rather than duplicate secrets. Ordinary status cannot reconstruct PSKs/passwords/private keys; public fingerprints cannot recreate an identity. Do not collect verifier material, session cookies or tickets for backup.
Reconstruction means first-admin provisioning, manually re-entering reviewed settings and secrets, re-importing authorized **public** keys, explicitly saving each configuration domain, and verifying new server trust. Replacement hardware/newly generated identities require client trust changes, not promises to restore the old identity. Test the reconstruction on an isolated spare with synthetic credentials. Exceptional forensic flash capture requires separate approval, restricted handling and retention; it remains secret-bearing, not a supported restore format.
## Recovery — smallest affected domain first
Use UART0 status/error observations to distinguish connectivity, authentication, storage and lifecycle faults. Avoid reset for ordinary login throttling or protocol mismatch; restrict hostile traffic and allow natural budget refill. Native USB preserves serial access during network failure, not administrative recovery.
| Fault | Deliberate recovery and scope |
|---|---|
| Wrong/lost password, healthy database | `user password <username>` through UART0 or another current admin; inspect/revoke authorized keys separately if compromised. Do not recover/erase the database. |
| User database unavailable after failed initialization | Only with explicit approval, UART0 `user recover --force` replaces **only the user blob** with an empty database, discarding all accounts/verifiers/authorized keys. Then `user add <username> admin`. Healthy databases, including healthy empty ones, are refused. Underlying NVS/RNG/allocation failure can still prevent recovery. Empty storage commits before dummy-verifier initialization; a later initialization error does not prove the old accounts survived. |
| HTTPS material unavailable/damaged | With approval, UART0 `web reset --force` replaces **only HTTPS certificate/private key**, not users or SSH/Wi-Fi configuration; it can also replace healthy material. Unlike stopped rotation, reset attempts to start a stopped service. Verify new trust and lifecycle state. |
| SSH material unavailable/damaged | With approval, UART0 `ssh reset --force` replaces **only SSH host identity**, not account keys/passwords or HTTPS material; it can also replace healthy material and attempts startup. Verify new trust and lifecycle state. |
| Ordinary configuration wrong/incompatible | Review the domain's `status`, then choose `load`, manual correction plus `save`, or explicitly approved domain `reset`. `serial reset`, `wifi reset`, `mdns reset`, `display reset` are not factory resets; Wi-Fi reset can reopen fallback access with a new password. |
| Service/network fault with healthy identity | Recover radio/profile or stop/start the affected service; preserve identities. If initialization remains unavailable, investigate the reported dependency/storage fault before an approved reboot or broader recovery. |
Identity resets are also available to authenticated admin SSH, but UART0 is preferred for recovery; browser reset is unavailable. None of these domain commands promises secure deletion of superseded flash bytes. A failed write or lost result calls for inspection, not assumptions about persisted state.
## Destructive factory-style reset — explicit approval only
There is **no on-device factory-reset command**. Full reset is the host-side erase/reflash procedure, not a collection of identity resets. It is not a routine upgrade, backup, password repair or prerequisite for this runbook.
1. Obtain explicit approval for this particular target and **all flash/NVS data loss**; confirm the physical device/port, maintenance window, protected reconstruction inventory and reviewed firmware. Disconnect sensitive serial equipment and isolate RF before erase/reflash.
2. Only in that separately authorized hardware session, from the repository root, run `pio run --target erase`; check success, then reflash with `pio run --target upload`. Use `pio device monitor -b 115200` on the USB-to-UART bridge for provisioning. Do not automate or run these operations as documentation validation.
3. Erase removes firmware and all NVS domains, including users, Wi-Fi secrets/configuration, TLS/SSH identities, serial, mDNS and display settings, plus other flash contents. Ordinary upload alone is **not** a reset. Reflash and follow UART0-first provisioning; expect newly generated identities/AP secret and no old accounts. Re-establish client trust only after verification.
4. Erase success is **not a forensic secure-erase guarantee**, proof that external copies disappeared, or a general persistence guarantee. Preserve custody/disposal controls below. See the [documented partition migration](../README.md#one-time-migration-from-the-default-partition-table) before changing layouts.
## Decommissioning
- Isolate/disconnect the device and attached serial equipment; revoke access **outside the device** before custody changes. Rotate upstream/shared Wi-Fi PSKs or revoke network admission as applicable; remove saved device-AP credentials on peers.
- Remove retired device certificate exceptions/pins and SSH known-host trust on operator systems. Revoke account/key authorizations and reused credentials on other devices/services separately; a local reset cannot revoke remote trust or a copied private key.
- If approved, perform the destructive erase above; do not reflash merely to retire hardware. Logical rotation/reset can leave historical plaintext NVS pages, older credentials and private keys in flash. Retired flash/RAM captures, backups, terminal records, vault versions and workstation copies require separate retention/disposal handling.
- Flash erase, file deletion and software RAM wiping do not prove all copies are unrecoverable. Maintain physical custody; use approved media destruction/device disposal when confidentiality requires it. Do not claim secure boot, encrypted NVS, extraction resistance or guaranteed erasure.
## Deferred whole-Phase-9 hardware rehearsal
Use a separately authorized, isolated expendable target and synthetic secrets. Record sanitized outcomes, exact tested build and limitations, not passwords, raw dumps or live identifiers. These checks are **planned, not passed**, and join the [combined Phase 9 gates](security_hardening.md#combined-phase-9-target-validation--deferred-not-run) and [administration regressions](user_administration_tests.md):
- [ ] First boot/healthy-empty provisioning; no remote first-admin path; fallback AP protection, saved radio-off/on behavior, and fresh trusted HTTPS/SSH login for both roles.
- [ ] Save/load/default/reset/reboot per configuration domain; failed first-default save and incompatible Wi-Fi storage; compare runtime radio/listener state with saved boot intent.
- [ ] Password/key/role/delete rotation: old credential rejection, self-revocation, unrelated-account isolation, final-admin protection, remote self-generated denial, typed generation-before-submit, lost/partial one-time output and stale results.
- [ ] Both identity rotations running/stopped; failed stop/commit/restart and lost ACK/result using reviewed fault injection; no blind replay, stored-versus-served trust comparison, reconnect/relogin and independent UART0/USB access.
- [ ] Each domain recovery preserves unrelated domains; unavailable-user recovery is UART0-only, healthy recovery refused. Reconstruct a spare from protected records without raw NVS restore.
- [ ] Separately approved destructive erase/reflash rehearsal: all domains reset, fresh identities/AP credential/accounts provisioning; rehearse external trust revocation and disposal checklist without claiming secure erasure.
- [ ] Complete existing whole-phase crash/no-dump, authentication/admission/refill, hidden-input, protocol/rekey, allocation-failure and full transport-mix/resource checks; verify one writer, isolated observers and recovery. Do not substitute this runbook review for those measurements.
Source basis: [startup](../src/main.c), [console policy](../src/admin_ssh_console.c), [users](../src/user_console.c) / [storage](../src/user_database.c), [Wi-Fi commands](../src/wifi_console.c) / [manager](../src/wifi_manager.c) / [storage](../src/wifi_config.c), [HTTPS owner](../src/web_server.c) / [material](../src/web_security.c), and [SSH owner](../src/ssh_transport.c) / [material](../src/ssh_security.c). Commit ordering is an implementation contract, not power-loss or hardware-validation evidence.
+172
View File
@@ -0,0 +1,172 @@
# Phase 9D — wolfSSL / wolfSSH advisory review (partial)
## Implementation addendum — 2026-09-15
**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.
The CVE-2025-12888 configuration mitigation is no longer merely proposed. Supplied parent evidence: `pio run` **PASS**, **94,340 B linked RAM / 1,767,217 B flash**, **64,092 B flash** from 9C with unchanged linked RAM; all four [focused mitigation commands](security_hardening.md#mitigation-hostbuild-evidence--2026-09-15) passed. Independent reviewer reports SDK-override and wolf-crypto-policy tests passed with no blocking implementation defects. These are supplied host/build results, not reruns by this documentation update, device timing measurements, real SSH interoperability/rekey evidence or runtime-headroom evidence.
**Still pending:** coherent wolfSSH message-order review/correction (CVE-2025-14942), IGNORE/service and other parser review, ECC import/ECDH/raw-signature validation review, remaining advisory applicability, and whole-phase target validation. No dependency upgrades were made; newer release pairs below remain unvalidated candidates. License/source/notice packaging remains unresolved in the [license inventory](dependency_licenses.md). Phase 9 is not complete or production-ready.
## Historical pre-mitigation research baseline — 2026-09-15
**The remainder retains the original research evidence. “Current” macros, generated hashes and “not applied/tested” statements below refer to the earlier non-small snapshot; the addendum supersedes those implementation-status claims only. The pending ordering/parser/ECC findings are not closed.**
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.
## Release decision / current-path priorities
1. **Block security sign-off pending mitigation of CVE-2025-12888.** Current SSH advertises X25519 on Xtensa, and actual compiler preprocessing confirms the affected non-small implementation. Upstream recommends the small implementation; existing blinding is not evidence that this separate compiler/architecture issue is resolved. Exploitability on this precise GCC/optimization/device combination was not measured.
2. **Block sign-off pending a reviewed wolfSSH message-order correction.** CVE-2025-14942 is critical for clients; upstream explicitly recommends its fix for servers because the same defect exists, without identifying a specific server attack. The generated local server state machine lacks the fix. Do **not** report demonstrated server password disclosure or authentication bypass from this evidence.
3. **Close current-path parser gaps before sign-off.** Upstream wolfSSH PR 892 fixes unchecked SSH_IGNORE/service-string parsing still present in the generated source. These are security-relevant malformed-input handling defects, not assigned CVEs in the inspected release notes. Actual out-of-allocation access/exploit impact remains unproven: packet padding and enclosing receive-buffer invariants need auditing. Phase 9C's password fix does not fix these handlers.
4. **Keep ECC validation and other non-CVE hardening open.** Current ECC import lacks `WOLFSSL_VALIDATE_ECC_IMPORT`; 5.9.1 release notes explicitly recommend this flag for older versions. Whether the SSH ECDH path already performs equivalent validation before scalar multiplication was not resolved. Do not call this a confirmed invalid-curve vulnerability.
Smallest proposed mitigation for item 1: enable `CURVE25519_SMALL` consistently for library and consumers through project-owned compile policy; upstream PR 9275 also enables `ED25519_SMALL`, `CURVE448_SMALL`, and `ED448_SMALL` on Xtensa. For enabled algorithms, follow that upstream policy rather than assume X25519 blinding suffices. Check the effective blinding/small combination, ABI, compile guards, performance, and real SSH interoperability. This was **not applied or tested**.
Item 2 is **not a safe one-line patch**. PR 855 changes `internal.c`, handshake layout in `wolfssh/internal.h`, message ranges, expected-message tracking, and logging; 1.4.20 also predates intervening rekey changes. A backport needs the coherent library/header change set, audit of prerequisites, and later PR 921 follow-up noted in 1.5.0. Prefer evaluating **wolfSSH 1.5.0 + wolfSSL 5.9.2**, the newest stable releases returned by the queried APIs, rather than merely reaching the minimum fixed release. This is a candidate pair, **not verified ESP managed-component availability or compatibility**. Any upgrade requires rebasing exact-hash Phase 9C overrides, auth/version guards, callback changes (including keyboard auth), and contract tests; do not blindly repin.
For item 3 the smallest candidate backport is the bounded `GetSkip`/`GetString` handling from PR 892, including necessary helper semantics, plus regression cases for truncated/oversized/wrapping lengths. Preserve Phase 9C password framing and wiping. No backport has been proven safe by this report.
## Local evidence and effective policy
- `dependencies.lock`: `wolfssl/wolfssl` **5.8.2~1** (upstream **5.8.2**), component hash `4d619e882c19d967bbaa53302e3bd2bdb8c611b5efb13302171aace809fccfdf`; `wolfssl/wolfssh` **1.4.20**, component hash `24d623360c07374a90b1ade8d1218b24bb7b661fd51ace9135dccf510a338927`.
- Actual database: `.pio/build/esp32-s3-devkitc-1-n16r8/compile_commands.json`. Replayed four actual compiler commands with `-E -dM`, removing compilation/output/dependency-write flags, capturing stdout only: wolfCrypt `ecc.c`, `curve25519.c`, `random.c`, and generated wolfSSH `security_overrides/wolfssh_internal/internal.c`. All four preprocessing invocations returned 0. This is current compile-policy evidence, not proof the flashed binary matches it.
- Compiler: `xtensa-esp32s3-elf-gcc`, `__GNUC__=14`, `__GNUC_MINOR__=2`, `__XTENSA__=1`; representative wolfCrypt commands use **`-Og`**.
- Generated wolfSSH input SHA-256: `1c04d26d46f7bef61eefcf0b78c43742662001bf6054ad2383500c9063c62494`. `tools/security_overrides.py` pins original `internal.c` SHA-256 `81ff1f9166708abd5c2911e9fe57c0aee01c88b5d3f68c909ee8a856d37f36a9`; edits bound both password lengths with `GetSize`, prevent malformed new-password framing reaching the callback, and wipe the checked method suffix. No message-order or IGNORE/service parsing correction appears in those edits.
- `src/ssh_protocol_policy.c`: KEX `curve25519-sha256,ecdh-sha2-nistp256`; host key `ecdsa-sha2-nistp256`; ciphers AES-128/256-GCM; MAC advertisement HMAC-SHA256; user-key advertisement Ed25519/P256. Database authorization remains separate from advertisement.
- `src/ssh_transport.c`: creates `WOLFSSH_ENDPOINT_SERVER`; sets authentication callbacks, explicit keyboard rejection, shell acceptance, and exec/subsystem rejection. Project memory identifies wolfCrypt as SSH crypto, **not the HTTPS provider**. However, **`WOLFCRYPT_ONLY` and `NO_TLS` are absent** in the actual macros: “used only for crypto” must not be mislabeled “TLS compiled out.” No duplicate IDF/mbedTLS advisory assessment is made here.
| Area | Effective observations | Consequence |
| --- | --- | --- |
| Math | `USE_FAST_MATH` present; `WOLFSSL_SP_MATH`, `WOLFSSL_SP_MATH_ALL`, `WOLFSSL_HAVE_SP_ECC`, `WOLFSSL_HAVE_SP_DH`, `WOLFSSL_SP_NONBLOCK` absent | TFM, not SP. SP-specific release fixes are not automatically current-path findings. |
| DH | `NO_DH` present; generated SSH also has `WOLFSSH_NO_DH` | Finite-field DH and SSH DH-GEX fixes are not current negotiated features. ECDH is distinct and remains enabled. |
| ECC | `HAVE_ECC`, `ECC_TIMING_RESISTANT`, `HAVE_ECC_CHECK_KEY` present; `WOLFSSL_VALIDATE_ECC_IMPORT`, `USE_ECC_B_PARAM` absent; `HAVE_ECC_CHECK_PUBKEY_ORDER` appears in `ecc.c` after its internal includes | A compiled key-check implementation is not proof callers invoke it. `ecc.c:1099310996` gates import-time checking on the absent validation flag. |
| X25519 / Ed25519 | `HAVE_CURVE25519`, `HAVE_ED25519`, `WOLFSSL_CURVE25519_BLINDING` present; `CURVE25519_SMALL`, `ED25519_SMALL` absent | CVE-2025-12888 configuration matches; earlier blinding hardening is enabled. |
| RNG | `HAVE_HASHDRBG`, `WC_RNG_SEED_CB` present; custom generate-block/seed macros absent; `wc_SetSeed_Cb(ssh_seed)` registration observed | Project callback supplies wolfCrypt Hash-DRBG seeding. Do not confuse this with OpenSSL RAND-after-fork or claim entropy quality was tested. |
| AES / hardware | `HAVE_AESGCM` present; streaming GCM absent; ESP32 crypto port present, hardware AES/hash explicitly disabled; RSA hardware-disable macro absent | Current AES/hash software policy is intentional. Not an all-software-crypto claim. |
| Optional APIs | `HAVE_PKCS7`, `HAVE_ECCSI`, `HAVE_CHACHA`, `HAVE_XCHACHA`, `HAVE_POLY1305`, `OPENSSL_EXTRA` absent; ASN template present/original absent | Many crypto-only advisories still need feature/API filtering, not just TLS exclusion. |
| SSH features | `WOLFSSH_CERTS`, `WOLFSSH_SFTP`, `WOLFSSH_SCP`, `WOLFSSH_FWD`, `WOLFSSH_AGENT` absent; `NO_WOLFSSH_CLIENT` absent | No certificate/file-transfer/forwarding feature; client code not compile-disabled, but inspected application is server-only. |
## Priority advisory evidence table
Dates below are CVE record **publication dates (UTC)**, not reservation or release dates. Upstream severity does not by itself establish local exploitability. Source URLs are listed below.
| ID / upstream severity | Published | Affected / fixed upstream | Local applicability and evidence |
| --- | --- | --- | --- |
| **CVE-2025-12888 / Low** | 2025-11-21 | `<5.8.4`; fixed 5.8.4, wolfSSL PR 9275 | **Applicable configuration, immediate mitigation priority.** Xtensa + advertised X25519 + small implementation absent. Patch inspected. |
| **CVE-2025-14942 / Critical (client)** | 2026-01-06 | `<1.4.22`; fixed 1.4.22, wolfSSH PR 855 | **Potential server impact / upstream recommends server fix.** Generated `internal.c:587623` has old accept-state filtering, no expected-message tracking. Critical client credential-leak scenario is not this application's role. |
| CVE-2025-11625 / Critical | 2025-10-21 | `<=1.4.20` client; fixed 1.4.21, PR 840 | **Not current role.** Host-verification bypass/credential leak concerns wolfSSH clients. CNA version string is malformed (`1.4.20;0`); range taken from official release notes. |
| CVE-2025-11624 / Medium | 2025-10-21 | 1.3.01.4.20 inclusive; fixed 1.4.21, PR 834 | **Not current feature.** SFTP file-handle stack overflow; SFTP absent. |
| CVE-2025-15382 / Medium | 2026-01-06 | Release notes: 1.4.121.4.21 inclusive; fixed 1.4.22, PR 859 | **Not current feature.** SCP clean-path overread. CNA incorrectly/differently says `<1.4.21`; use conservative release-note range and retain discrepancy. |
| CVE-2026-0930 / Low | 2026-04-20 | 1.4.15`<1.5.0`; fixed 1.5.0 | **Not current platform/application.** Windows wolfSSHd terminal resize. Release notes name PR 864; CNA references PR 846 instead. Discrepancy not resolved. |
| CVE-2026-5194 / Critical | 2026-04-09 | 3.12.0`<5.9.1`; fixed 5.9.1, wolfSSL PR 10131 | **Not demonstrated on current SSH flow.** Certificate OID/type confusion absent without SSH certificates. Patch also hardens low-level ECC digest lengths, so crypto-only use is not a blanket exclusion. Inspected SSH paths derive digest sizes from hash IDs; finish tracing every current raw-signature call before closure. |
| CVE-2026-5187 / Low | 2026-04-09 | `<=5.9.0`; fixed 5.9.1, PR 10025 | **No current trigger established.** `DecodeObjectId`/unknown ASN.1 extension handling; raw SSH keys are not X.509 extensions. ASN is compiled for keys/signatures, so retain API reachability check rather than claim all ASN parsing absent. |
| CVE-2025-13912 / Low | 2025-12-11 | `<5.8.4`; fixed 5.8.4, PR 9148 | **Not reported compiler/architecture combination.** Advisory describes Clang on AArch64/RISC-V/x86_64; current GCC14.2 Xtensa. Separate from CVE-2025-12888. |
| CVE-2025-7396 / Low | 2025-07-18 | Blinding introduced 5.8.0, enabled by default in applicable 5.8.2 builds | **Existing mitigation present:** actual `WOLFSSL_CURVE25519_BLINDING`. CNA affected string `5.8.0; 0` is not a usable precise range. Not a resolution of the Xtensa advisory. |
## Post-pin release review, including changes without CVEs
| Product / version | Release-note date / GitHub publication UTC date | Scope checked |
| --- | --- | --- |
| wolfSSL 5.8.4 | 2025-11-20 / 2025-11-21 | Xtensa small implementations, Clang hardening, TLS advisories, ESP SHA changes. |
| wolfSSL 5.9.0 | 2026-03-18 / 2026-03-19 | TLS/PKCS/CRL advisories; RISC-V SP timing, KCAPI ECC import, RNG/port changes. |
| wolfSSL 5.9.1 | 2026-04-08 / 2026-04-08 | Certificate/digest checks; ECC import validation; SP and hardware-port hardening. |
| wolfSSL 5.9.2 | 2026-06-23 / 2026-06-25 | Latest stable returned; additional crypto validation, zeroization, SP/DH, PKCS, TLS and platform fixes. Unreleased master material is not a release candidate. |
| wolfSSH 1.4.21 | 2025-10-20 / 2025-10-22 | Client/SFTP CVEs; rekey PR 793, auth rejection fixes, keyboard callback changes. |
| wolfSSH 1.4.22 | 2026-01-05 / 2026-01-06 | State-machine/SCP CVEs; PRs 854 auth initialization, 857 string parsing, 856 worker/backpressure. |
| wolfSSH 1.5.0 | 2026-04-17 / 2026-04-20 | Latest stable returned; parser/bounds, callback defaults, key cleanup, KEX ordering, Windows CVE. |
Non-CVE findings are not automatically less important:
- **wolfSSH PR 892 (fixed in 1.5.0):** actual generated `DoIgnore` at line 5912 reads its length with unchecked `ato32`, ignores `len`, and advances by an unchecked peer length. `DoServiceRequest` at line 6124 also reads before checking; later service-name check does not establish four readable input bytes. Patch replaces these with bounded helpers. Password portion is already locally corrected, but the entire PR is not present.
- **wolfSSH PR 881 (1.5.0):** patch adds overflow check before `peerWindowSz += bytesToAdd`. Patch inspected; exact local function comparison was not completed. Potential availability/correctness issue, not a confirmed buffer overflow.
- **wolfSSH PR 899 (1.5.0):** patch bounds skips in `ParseRSAPubKey`/`ParseECCPubKey` and corrects `DoChannelFailure` length validation. Current role/reachability not finished; do not conflate client host-key parsing with server user-key authentication.
- **wolfSSH PR 880 (1.5.0):** release notes describe public-key type validation logical-operator correction. Actual generated `DoUserAuthRequestEcc` contains length mismatch **AND** `memcmp` mismatch checks for both key and signature type. This needs focused review with database key matching and attacker signature framing; exploit impact not established. Terminal-mode, agent, TPM, daemon changes in that PR need separate feature filtering.
- **wolfSSH PRs 918/919/902 (1.5.0):** forwarding/channel callback and payload hardening. FWD is absent; shell/exec/subsystem callbacks are registered, but default channel-open handling is not fully audited. No forwarding bypass claim.
- **wolfSSL PR 10133 (5.9.1):** ECC validation hardening. Actual `wc_ecc_import_x963_ex` calls `wc_ecc_check_key` only under absent `WOLFSSL_VALIDATE_ECC_IMPORT`. `HAVE_ECC_CHECK_PUBKEY_ORDER` is present inside `ecc.c`; its validation implementation can load curve B even without `USE_ECC_B_PARAM`. Do not infer “no point check anywhere” merely from absent B macro. Smallest candidate hardening is the upstream-recommended validation flag, pending full ECDH call trace and cost tests.
- **wolfSSL 5.9.2:** SP integer fixes, P521 SP ECDH 65/66-byte output check (PR 10702), DH subgroup hardening (PR 10560) are not current TFM/P256/no-DH features. Curve25519 all-zero-secret default checking (PR 10374), private-key clamp check (PR 10363), and broader ECC/EdDSA zeroization/validation fixes warrant follow-up. ESP port fixes in 5.9.1 PRs 10003/10080 need per-backend review; AES/hash hardware is disabled but that does not eliminate every ESP port path.
## Remaining wolfSSL CVE inventory collected
All identifiers in this section have the prefix **CVE-**. Ranges are CNA metadata, not proof of local reachability; `0` means the record supplied no meaningful historical lower bound. Every listed record was successfully queried at `https://cveawg.mitre.org/api/cve/<full-ID>`. Fixed versions and trigger descriptions came from the official security page and ChangeLog. This is a compact screening inventory, **not completed per-patch validation**.
| IDs | Published | Affected range / fixed | Current-feature screening |
| --- | --- | --- | --- |
| 2025-11931 | 2025-11-21 | `<5.8.4` / 5.8.4 | XChaCha API absent. |
| 2025-11932, 2025-12889 | 2025-11-21 | `<5.8.4` / 5.8.4 | wolfSSL TLS PSK/digest policy not used. |
| 2025-11933, 2025-11934, 2025-11935, 2025-11936 | 2025-11-21 | `[3.12.0,5.8.4)` / 5.8.4 | wolfSSL TLS1.3 not used. |
| 2026-0819 | 2026-03-19 | `[5.5.0,5.9.0)` / 5.9.0 | PKCS7 absent. |
| 2026-1005, 2026-2646 | 2026-03-19 | `<=5.8.4` / 5.9.0 | No wolfSSL sniffer/session restore use. |
| 2026-2645 | 2026-03-19 | `<5.8.4` / 5.8.4; further 5.9.0 hardening | wolfSSL TLS1.2 server not used; table headline 5.9.0 must not obscure earlier effective fix. |
| 2026-3229, 2026-3230, 2026-3547, 2026-3548, 2026-3549 | 2026-03-19 | `<5.9.0` / 5.9.0 | Compatibility certificate APIs/TLS/CRL not current SSH features. |
| 2026-3503 | 2026-03-19 | `[5.8.2,5.9.0)` / 5.9.0 | ARM Cortex-M PQ fault attack; wrong platform/features. |
| 2026-3579, 2026-3580 | 2026-03-19 | `<5.9.0` / 5.9.0 | RISC-V RV32I SP timing; wrong platform/math. |
| 2026-3849 | 2026-03-19 | 5.6.05.8.4 inclusive / 5.9.0 | HPKE/ECH TLS client not used. |
| 2026-4159, 2026-4395 | 2026-03-19 | `<5.9.0`; `<=5.8.4`, respectively / 5.9.0 | PKCS7 / Linux KCAPI ECC not current features. |
| 2026-5188 | 2026-04-10 | `<=5.9.0` / 5.9.1 | Original ASN SAN parser; current template parser, no SSH certificates. |
| 2026-5263, 2026-5264, 2026-5295, 2026-5392, 2026-5393, 2026-5447, 2026-5448, 2026-5460 | 2026-04-09 | `<5.9.1` / 5.9.1 | Certificate constraints, DTLS, PKCS7, experimental dual certs, compatibility APIs or TLS PQ paths not used. |
| 2026-5446 | 2026-04-09 | `[5.2.1,5.9.1)` / 5.9.1 | ARIA TLS not used. |
| 2026-5466, 2026-5479 | 2026-04-10 | `<5.9.1` / 5.9.1 | ECCSI / EVP ChaCha absent. |
| 2026-5477, 2026-5500, 2026-5501 | 2026-04-10 | `<=5.9.0` / 5.9.1 | EAX/CMAC >4GiB, PKCS7, compatibility X509 not used. |
| 2026-5503, 2026-5504, 2026-5507, 2026-5772, 2026-5778 | 2026-04-09 | `<=5.9.0` / 5.9.1 | ECH, PKCS7, session restore, hostname matcher, sniffer not used. |
| 2026-10097 | 2026-06-25 | `[5.7.0,5.9.1]` / 5.9.2 | AVX2 ML-KEM; wrong platform/feature. |
| 2026-10098 | 2026-06-25 | `[4.6.0,5.9.1]` / 5.9.2 | OCSP not used. |
| 2026-10512 | 2026-06-25 | `[5.6.4,5.9.1]` / 5.9.2 | X25519 x86_64 assembly; wrong implementation. |
| 2026-10592, 2026-6731, 2026-6412 | 2026-06-25 | `[3.9.10,5.9.1]` / 5.9.2 | Certificate name constraints/digest policy not current feature. |
| 2026-11310 | 2026-06-25 | `[5.8.4,5.9.1]` / 5.9.2 | Pin predates introduction; compatibility X509 absent. |
| 2026-11703 | 2026-06-25 | `[3.15.0,5.9.1]` / 5.9.2 | TLS SNI/ALPN resumption not used. |
| 2026-11999, 2026-6091, 2026-55964 | 2026-06-25 | `[5.7.4,5.9.1]` / 5.9.2 | Certificate path verification not current feature. |
| 2026-12340, 2026-55960 | 2026-06-25 | `[5.6.4,5.9.1]` / 5.9.2 | SM2 certificate/RPK TLS not used. |
| 2026-55958 | 2026-06-25 | `[5.4.0,5.9.1]` / 5.9.2 | Renesas TSIP TLS; wrong platform. |
| 2026-55961 | 2026-06-25 | `[3.15.7,5.9.1]` / 5.9.2 | Compatibility PKCS7 absent. |
| 2026-55962 | 2026-06-25 | `[5.5.4,5.9.1]` / 5.9.2 | TLS post-handshake authentication not used. |
| 2026-55967 | 2026-06-25 | `[4.8.0,5.9.1]` / 5.9.2 | GCM streaming >64GiB per message; streaming absent, not cumulative SSH session traffic. |
| 2026-6092 | 2026-06-25 | `[5.2.0,5.9.1]` / 5.9.2 | TLS Encrypt-then-MAC not used. |
| 2026-6094, 2026-7531 | 2026-06-25 | `[5.8.0,5.9.1]` / 5.9.2 | PKCS7 / TLS PQ hybrid not used. |
| 2026-6291 | 2026-06-25 | `[3.9.10,5.9.1]` / 5.9.2 | PKCS7 RSA padding oracle, not SSH raw key authentication. |
| 2026-6325 | 2026-06-25 | `[4.8.0,5.9.1]` / 5.9.2 | TLS signature-list setup not used. |
| 2026-6329 | 2026-06-25 | `[3.10.0,5.9.1]` / 5.9.2 | PKCS12 MAC verification not a current SSH workflow. |
| 2026-6330 | 2026-06-25 | `[5.7.4,5.9.1]` / 5.9.2 | ARM64 ML-KEM; wrong platform/feature. |
| 2026-6331, 2026-6678, 2026-7511 | 2026-06-25 | `[3.15.5,5.9.1]` / 5.9.2 | EVP HMAC / PKCS7 APIs not used. |
| 2026-6450 | 2026-06-25 | `[4.3.0,5.9.1]` / 5.9.2 | CRL not used. |
| 2026-6679 | 2026-06-25 | `[5.4.0,5.9.0]` / 5.9.1 | DTLS not used. Listed under 5.9.2 but narrative says already fixed 5.9.1. |
| 2026-6681 | 2026-06-25 | `[3.10.0,5.9.0]` / 5.9.1 | PKCS7 absent; same release-table/narrative discrepancy. |
| 2026-7532 | 2026-06-25 | `<=5.9.1` / 5.9.2 | X509 IP constraints not used. Related advisory ID TALOS-2026-2409 appears in upstream ChangeLog; Talos page not queried. |
| 2026-8720 | 2026-06-25 | `[5.9.0,5.9.1]` / 5.9.2 | HMAC-BLAKE2 APIs introduced after pin. |
## Queried sources and caveats
All following requests occurred during this **2026-09-15** review. Network requests succeeded; no blocked-network limitation applies to collected external evidence. The last attempted local read/status command failed with `authorization channel closed`; no final repository status/diff validation was obtained.
Official index and release sources fetched:
- https://www.wolfssl.com/docs/security-vulnerabilities/
- https://github.com/wolfSSL/wolfssh/releases
- https://raw.githubusercontent.com/wolfSSL/wolfssh/master/ChangeLog.md
- https://raw.githubusercontent.com/wolfSSL/wolfssl/master/ChangeLog.md
- https://api.github.com/repos/wolfSSL/wolfssl/releases?per_page=6
- https://api.github.com/repos/wolfSSL/wolfssh/releases?per_page=6
Patch URLs actually queried (read only; no patch applied):
- https://github.com/wolfSSL/wolfssl/pull/9275.diff
- https://github.com/wolfSSL/wolfssl/pull/10131.diff
- https://github.com/wolfSSL/wolfssl/pull/10133.diff
- https://github.com/wolfSSL/wolfssl/pull/10025.diff
- https://github.com/wolfSSL/wolfssh/pull/855.diff
- https://github.com/wolfSSL/wolfssh/pull/892.diff
- https://github.com/wolfSSL/wolfssh/pull/881.diff
- https://github.com/wolfSSL/wolfssh/pull/899.diff
CVE metadata: exact URL construction is `https://cveawg.mitre.org/api/cve/` followed by each full CVE identifier in the priority table and inventory above. Every such ID was queried; some twice. These are vendor CNA records served by the CVE API. Full paths are mechanically recoverable without search, e.g. https://cveawg.mitre.org/api/cve/CVE-2025-12888 and https://cveawg.mitre.org/api/cve/CVE-2025-14942 . No NVD or GitHub advisory API query was made.
Additional upstream patch links **identified in release notes, not fetched**: wolfSSH [840](https://github.com/wolfSSL/wolfssh/pull/840), [834](https://github.com/wolfSSL/wolfssh/pull/834), [859](https://github.com/wolfSSL/wolfssh/pull/859), [864](https://github.com/wolfSSL/wolfssh/pull/864), [793](https://github.com/wolfSSL/wolfssh/pull/793), [854](https://github.com/wolfSSL/wolfssh/pull/854), [856](https://github.com/wolfSSL/wolfssh/pull/856), [857](https://github.com/wolfSSL/wolfssh/pull/857), [880](https://github.com/wolfSSL/wolfssh/pull/880), [902](https://github.com/wolfSSL/wolfssh/pull/902), [918](https://github.com/wolfSSL/wolfssh/pull/918), [919](https://github.com/wolfSSL/wolfssh/pull/919), [921](https://github.com/wolfSSL/wolfssh/pull/921); wolfSSL [9148](https://github.com/wolfSSL/wolfssl/pull/9148), [10003](https://github.com/wolfSSL/wolfssl/pull/10003), [10080](https://github.com/wolfSSL/wolfssl/pull/10080), [10363](https://github.com/wolfSSL/wolfssl/pull/10363), [10374](https://github.com/wolfSSL/wolfssl/pull/10374), [10560](https://github.com/wolfSSL/wolfssl/pull/10560), [10702](https://github.com/wolfSSL/wolfssl/pull/10702).
Evidence limitations:
- The consolidated official page omitted the four wolfSSH CVEs published with 1.4.21/1.4.22 despite listing older and newer entries. **Neither that omission nor the user's earlier empty GitHub advisory tabs proves absence of vulnerabilities.** GitHub advisory tabs were not re-queried here.
- Release-note dates differ from GitHub publication dates. CNA metadata has malformed ranges and conflicting references as noted. No date was inferred from a CVE year.
- Master ChangeLogs and PR diffs are mutable. Release tags/timestamps were queried, but upstream full commit hashes and archived evidence snapshots were not captured. Some long patch responses were output-limited; do not interpret a successful fetch as a complete hunk-by-hunk audit.
- Macro evidence is stronger than default Kconfig assumptions but not disassembly/timing evidence. Whole-library API reachability/linker elimination, every math/port fix, low-order X25519 behavior, RNG error paths, and ECC validation call chains remain unfinished.
- Older entries such as wolfSSH CVE-2024-2873 (fixed 1.4.17) were visible in the index; this report is focused on post-pin maintenance and does not certify every historical advisory.
- Parent follow-up: resolve the current-path priorities, choose a coherent upgrade/backport strategy, add negative message-order/parser/key-validation tests against actual generated sources, and run firmware plus whole-Phase-9 hardware gates. No hardware evidence is implied here.
+55 -9
View File
@@ -15,6 +15,9 @@ configuration; there is no unpatched fallback or automatic hash repinning.
Derived **full files**, retaining the original copyright/license notices, live
only at `<CMAKE_BINARY_DIR>/security_overrides/<entry-name>/<basename>`.
Every generated source is centrally prefixed with an explicit
`Modified by the ESP32_serial_swiss_army_knife project on 2026-09-15` notice.
This is separate from, and does not rewrite or replace, upstream notices.
The manifest and derived files are atomically replaced only when their bytes
change. Output paths cannot escape the binary tree or alias SDK/source files.
CMake tracks the generator, version header, originals, and derived sources for
@@ -51,7 +54,7 @@ Source generator expressions are rejected rather than guessed through.
ECDSA/AES/GCM/SHA features are compile-guarded. IDF dynamic buffers are rejected
because their destructor bypasses the audited upstream record-buffer wipe.
`set_client_config()` and the shared handle/setup path are unchanged.
- `wolfssl__wolfssh:src/internal.c`: the fourth override pins wolfSSH 1.4.20's
- `wolfssl__wolfssh:src/internal.c`: pins wolfSSH 1.4.20's
original source SHA256 to
`81ff1f9166708abd5c2911e9fe57c0aee01c88b5d3f68c909ee8a856d37f36a9`.
`GetSize()` bounds password and new-password fields before authentication;
@@ -61,6 +64,26 @@ Source generator expressions are rejected rather than guessed through.
an async secret-lifetime/wipe guarantee. Generated parser/control-flow tests
live in `tests/wolfssh_auth_contract/`.
- `lwip:apps/dhcpserver/dhcpserver.c`: PAD/END handling, length-byte and
declared-payload bounds, minimum message-type/requested-IP lengths, validated
advancement. Backport of official
[d51b1076092487e533eadf8b48c9c8579d3a6712](https://github.com/espressif/esp-idf/commit/d51b1076092487e533eadf8b48c9c8579d3a6712.patch).
The only behavioral-code deviation is using equivalent remaining-length
comparisons rather than constructing pointers beyond the input object.
Original copyright years stay unchanged under the separate modification notice.
- Nested `mbedtls:ssl_tls.c`: propagate `calc_verify()` failure immediately,
before PRF/master-secret derivation. Exact TLS 1.2 hunk of
[f595df4569c1a1650ad9d077e2f2e819e9f1dddb](https://github.com/Mbed-TLS/mbedtls/commit/f595df4569c1a1650ad9d077e2f2e819e9f1dddb.patch).
EMS remains enabled. This does **not** backport that commit's TLS 1.3 hunk.
- Nested `mbedx509:x509_create.c`: reject known-attribute OID allocation failure
before `memcpy()`, returning `MBEDTLS_ERR_X509_ALLOC_FAILED`. Exact guard from
[bfaf4a47fd33da860796feaba6235847acb71127](https://github.com/Mbed-TLS/mbedtls/commit/bfaf4a47fd33da860796feaba6235847acb71127.patch).
These three patches were fetched from the official repositories and compared
with the installed pinned sources on 2026-09-15. No dependency versions or
existing original-source hashes changed. The optional WS subprotocol backport
and separate ASN.1 repeated-OID/empty-value correction are **not** implemented.
Clients that cannot negotiate this server profile will no longer connect.
Live interoperability and resource/latency testing remain hardware gates.
These corrections do not claim comprehensive zeroization of every TLS/library
@@ -75,14 +98,18 @@ Add an `Entry` to `tools/security_overrides.py:ENTRIES` with:
- `root="idf"` for installed IDF sources, or `root="project"` for project/vendor
sources;
- exact relative `source`, full reviewed `sha256`, and a tuple of `Edit(old,new)`
exact-once substitutions.
exact-once substitutions;
- optional `target` for the explicitly supported `mbedtls`, `mbedx509` or
`mbedcrypto` nested library under component `mbedtls`. Empty selects the
component's `COMPONENT_LIB` as before. CMake validates the nested target exists,
is not imported/an alias, and belongs to the installed mbedTLS library directory.
`render_entry()` validates/patches an entry; `generate()` accepts an explicit
entry tuple as well as the default registry. The manifest maps each entry to
its component, original and derived source. CMake's
`sak_security_replace_source(component original generated)` handles replacement
without backend assumptions. The current registry uses this for three IDF
sources and the project-managed wolfSSH source described above. Update the
its component, optional nested target, original and derived source. CMake's
`sak_security_replace_source(component original generated nested_target)` handles
replacement on the actual source owner, not the IDF mbedTLS wrapper. The current
registry has six IDF sources and one project-managed wolfSSH source. Update the
corresponding library-specific feature/behavior tests when extending the registry. Multiple
sources in the same real component are supported by the replacement function.
@@ -98,7 +125,8 @@ python3 tests/sdk_security_overrides/run.py --build-dir .pio/build/esp32-s3-devk
Optional `--idf-path` selects an existing installed SDK. Tests need host `cc`,
CMake and Ninja; they install nothing, use temporary directories under `.pio/`, and never edit
the selected SDK. The second command also checks the existing real firmware
the selected SDK. Run the second command **after the parent runs `pio run`**;
old generated copies lack the new entries/notice and must not pass. It checks real firmware
Ninja registration: exactly one compilation of each derived source, no original
compilation, and exact generated bytes. It does **not** run a firmware build.
@@ -106,7 +134,8 @@ Coverage:
- Generator full-source hashes, version, missing/duplicate/ambiguous inputs,
exact edit counts, validation-before-output, unchanged-byte/mtime idempotence,
unsafe output rejection, and preserved upstream notices.
unsafe output rejection, centrally applied dated modification notice exactly
once on every source, and preserved upstream opening notices.
- Extracted **patched actual functions**, not reimplemented cleanup logic:
HTTPS allocation failure matrix; handshake failure; post-handshake allocation
failure; HTTPD start failure; normal close/stop; failed stop preserving ownership.
@@ -123,8 +152,25 @@ Coverage:
version limits, renegotiation enabled/compiled-out variants, untouched default
and caller-provided client suites. Every required feature is individually
removed in compile-failure tests; dynamic-buffer enablement also fails.
- Extracted DHCP parser: 196,623 guard-page cases covering empty/lone-code,
every option code/length with truncated and complete payloads, PAD, END, short
type/IP, DISCOVER/REQUEST/DECLINE/RELEASE, renew, matching/mismatching IP.
Compare the remaining-length variant with upstream pointer-form checks on
safely padded backing storage; post-loop state behavior is retained.
- Extracted TLS master calculation: SHA-256/SHA-384-sized transcript doubles,
untouched output and no PRF on hash failure (including unchanged length 64),
successful EMS, PRF failure, non-EMS and resumed-session behavior. This does not
execute actual hashes, the full handshake driver, or its failure destructor.
- Extracted X.509 parser and its actual attribute table/helpers, compiled against
installed mbedTLS headers: fault each allocation across separate subject/issuer
lists, partial-list cleanup, second-attribute OID failure, and successful retry.
Named-data storage/free are allocation-counted doubles; certificate signing,
writer destruction, persistence/publication and old-identity retention are not
exercised here. The three new C harnesses use UBSan trap instrumentation;
this does not require the host UBSan runtime.
- The actual CMake include under fake IDF target discovery, including missing and
duplicate sources/targets. A separate real host compile tests the project-root
duplicate sources/targets, nested target owner validation, and exact Ninja
compilation registration on both `mbedtls` and `mbedx509` (not their wrapper). A separate real host compile tests the project-root
extension, child-directory relative `SOURCES`, quoted and source-specific
includes, source/target flags and per-config source definitions. Changing that
fixture's original file makes the next ordinary build reconfigure and reject
+78
View File
@@ -0,0 +1,78 @@
/* SPDX-License-Identifier: GPL-3.0-only */
#include <assert.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <sys/mman.h>
#include <unistd.h>
typedef uint8_t u8_t;
typedef uint16_t u16_t;
typedef int16_t s16_t;
typedef struct { uint32_t addr; } ip4_addr_t;
typedef struct { uint32_t client_address; bool renew; } dhcps_t;
struct dhcps_state { u8_t state; };
enum { DHCPS_STATE_IDLE, DHCPS_STATE_ACK, DHCPS_STATE_NAK, DHCPS_STATE_OFFER,
DHCPS_STATE_DECLINE, DHCPS_STATE_RELEASE };
/* SDK_DEFINES */
/* SDK_FUNCTIONS */
static unsigned cases;
static u8_t check(dhcps_t *ctx, const u8_t *data, size_t len)
{
size_t page = (size_t)sysconf(_SC_PAGESIZE);
u8_t *map = mmap(NULL, page * 2, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
assert(map != MAP_FAILED && len <= page);
assert(mprotect(map + page, page, PROT_NONE) == 0);
u8_t *input = map + page - len;
memcpy(input, data, len);
u8_t result = parse_options(ctx, input, (s16_t)len);
/* Upstream's pointer expressions stay within this larger backing object. */
u8_t padded[4096 + 258] = {0};
memcpy(padded, data, len);
assert(result == upstream_parse_options(ctx, padded, (s16_t)len));
assert(memcmp(input, data, len) == 0);
assert(munmap(map, page * 2) == 0);
cases++;
return result;
}
int main(void)
{
dhcps_t ctx = {.client_address = 0x04030201};
u8_t data[300] = {0};
assert(check(&ctx, data, 0) == DHCPS_STATE_IDLE);
for (unsigned code = 0; code < 256; code++) {
data[0] = code;
check(&ctx, data, 1);
for (unsigned length = 0; length < 256; length++) {
data[1] = length;
check(&ctx, data, 2);
check(&ctx, data, 2 + length);
if (length) check(&ctx, data, 1 + length);
}
}
memset(data, 0, sizeof(data));
assert(check(&ctx, data, sizeof(data)) == DHCPS_STATE_IDLE);
u8_t discover[] = {0, 0, 53, 1, 1, 255};
assert(check(&ctx, discover, sizeof(discover)) == DHCPS_STATE_OFFER);
u8_t request[] = {53, 1, 3, 50, 4, 0, 0, 0, 0, 255};
memcpy(request + 5, &ctx.client_address, 4);
assert(check(&ctx, request, sizeof(request)) == DHCPS_STATE_ACK);
request[5] ^= 1;
assert(check(&ctx, request, sizeof(request)) == DHCPS_STATE_NAK);
assert(check(&ctx, request, 3) == DHCPS_STATE_NAK);
ctx.renew = true;
assert(check(&ctx, request, 3) == DHCPS_STATE_ACK);
request[2] = 7;
assert(check(&ctx, request, 3) == DHCPS_STATE_RELEASE);
request[2] = 4;
assert(check(&ctx, request, 3) == DHCPS_STATE_DECLINE);
u8_t short_type[] = {53, 0, 255};
assert(check(&ctx, short_type, sizeof(short_type)) == DHCPS_STATE_IDLE);
for (unsigned len = 0; len < 4; len++) {
u8_t short_ip[] = {50, len, 1, 2, 3};
assert(check(&ctx, short_ip, len + 2) == DHCPS_STATE_IDLE);
}
u8_t end[] = {255, 53, 1, 1};
assert(check(&ctx, end, sizeof(end)) == DHCPS_STATE_IDLE);
printf("DHCP extracted parser: %u guard-page / upstream-equivalence cases PASS\n", cases);
}
+68
View File
@@ -0,0 +1,68 @@
/* SPDX-License-Identifier: GPL-3.0-only */
#include <assert.h>
#include <stdio.h>
#include <string.h>
#define MBEDTLS_SSL_EXTENDED_MASTER_SECRET
#define MBEDTLS_SSL_EXTENDED_MS_ENABLED 1
#define MBEDTLS_ERR_ERROR_CORRUPTION_DETECTED -1
#define MBEDTLS_SSL_DEBUG_MSG(...) ((void)0)
#define MBEDTLS_SSL_DEBUG_RET(...) ((void)0)
#define MBEDTLS_SSL_DEBUG_BUF(...) ((void)0)
typedef struct { int unused; } mbedtls_ssl_context;
typedef struct {
int resume, extended_ms;
unsigned char randbytes[64], premaster[128];
size_t pmslen;
int (*calc_verify)(const mbedtls_ssl_context *, unsigned char *, size_t *);
int (*tls_prf)(const unsigned char *, size_t, const char *, const unsigned char *, size_t, unsigned char *, size_t);
} mbedtls_ssl_handshake_params;
static int hash_error, prf_error, hash_calls, prf_calls;
static size_t hash_size, expected_seed;
static void mbedtls_platform_zeroize(void *p, size_t n) { memset(p, 0, n); }
static int verify(const mbedtls_ssl_context *ssl, unsigned char *out, size_t *len)
{
hash_calls++;
if (hash_error) return hash_error; /* Deliberately leave seed_len=64. */
*len = hash_size;
memset(out, 0x23, *len);
return 0;
}
static int prf(const unsigned char *p, size_t n, const char *label,
const unsigned char *seed, size_t len, unsigned char *out, size_t size)
{
prf_calls++;
assert(len == expected_seed && size == 48);
assert(strcmp(label, len == 64 ? "master secret" : "extended master secret") == 0);
for (size_t i = 0; i < len; i++) assert(seed[i] == (len == 64 ? 0x45 : 0x23));
if (prf_error) return prf_error;
memset(out, 0x67, size);
return 0;
}
/* SDK_FUNCTIONS */
int main(void)
{
mbedtls_ssl_context ssl = {0};
for (unsigned sha = 0; sha < 2; sha++) {
hash_size = sha ? 48 : 32;
for (unsigned mode = 0; mode < 5; mode++) {
mbedtls_ssl_handshake_params h = {.calc_verify = verify, .tls_prf = prf,
.pmslen = 32, .extended_ms = mode != 3, .resume = mode == 4};
memset(h.premaster, 0xab, sizeof(h.premaster));
memset(h.randbytes, 0x45, sizeof(h.randbytes));
unsigned char master[48]; memset(master, 0xcd, sizeof(master));
hash_calls = prf_calls = 0;
hash_error = mode == 0 ? -0x1234 : 0;
prf_error = mode == 2 ? -0x2345 : 0;
expected_seed = mode == 3 ? 64 : hash_size;
int ret = ssl_compute_master(&h, master, &ssl);
assert(ret == (mode == 0 ? hash_error : mode == 2 ? prf_error : 0));
assert(hash_calls == (mode == 3 || mode == 4 ? 0 : 1));
assert(prf_calls == (mode == 0 || mode == 4 ? 0 : 1));
for (size_t i = 0; i < sizeof(master); i++)
assert(master[i] == (mode == 1 || mode == 3 ? 0x67 : 0xcd));
for (size_t i = 0; i < sizeof(h.premaster); i++)
assert(h.premaster[i] == (mode == 1 || mode == 3 ? 0 : 0xab));
}
}
puts("EMS extracted master calculation: SHA256/SHA384 error, success, PRF failure, non-EMS, resumption PASS");
}
+89 -12
View File
@@ -10,6 +10,7 @@ import argparse
from dataclasses import replace
import hashlib
import importlib.util
import json
import os
from pathlib import Path
import re
@@ -91,8 +92,16 @@ def generator_tests(idf, work):
for entry in sdk.ENTRIES:
original = source_path(entry, idf).read_bytes()
derived = (binary / "security_overrides" / entry.name / Path(entry.source).name).read_bytes()
assert derived.startswith(original[:original.index(b"*/") + 2])
notice = sdk.MODIFICATION_NOTICE.encode()
assert derived.startswith(notice)
assert derived.count(notice) == 1
assert b"Modified by the ESP32_serial_swiss_army_knife project on 2026-09-15" in notice
assert derived[len(notice):].startswith(original[:original.index(b"*/") + 2])
assert derived != original
expect_error(lambda: sdk.render_entry(replace(sdk.ENTRIES[0], target="mbedtls"),
{"idf": idf, "project": ROOT}), "invalid nested target")
expect_error(lambda: sdk.render_entry(replace(sdk.ENTRIES[1], target="unknown"),
{"idf": idf, "project": ROOT}), "invalid nested target")
expect_error(lambda: sdk.apply_edits("x", (sdk.Edit("missing", "z"),)), "got 0")
expect_error(lambda: sdk.apply_edits("xx", (sdk.Edit("x", "z"),)), "got 2")
expect_error(lambda: sdk.generate(idf, ROOT, binary, ()), "absent")
@@ -162,6 +171,26 @@ def extracted_tests(idf, binary, work):
run(["cc", "-E", "-x", "c", *["-D" + f for f in FEATURES if f != feature], guard_file], ok=False)
run(["cc", "-E", "-x", "c", *defines, "-DCONFIG_MBEDTLS_DYNAMIC_BUFFER", guard_file], ok=False)
print("TLS feature guard matrix (each required feature + dynamic buffer rejection) PASS")
dhcp = texts["dhcpserver"]
parser = extract(dhcp, "parse_options")
upstream = parser.replace("parse_options(", "upstream_parse_options(", 1)
upstream = upstream.replace("end - optptr < 2", "optptr + 1 >= end")
upstream = upstream.replace("opt_len > end - optptr - 2", "optptr + 2 + opt_len > end")
names = ("DHCP_OPTION_PAD", "DHCP_OPTION_END", "DHCP_OPTION_MSG_TYPE",
"DHCP_OPTION_REQ_IPADDR", "DHCPDISCOVER", "DHCPREQUEST", "DHCPDECLINE", "DHCPRELEASE")
defines = "\n".join(re.search(r"^#define " + name + r"\s+[^\n]+", dhcp, re.M).group() for name in names)
source = (HERE / "dhcp.c").read_text().replace("/* SDK_DEFINES */", defines)
compile_run("dhcp", source.replace("/* SDK_FUNCTIONS */", parser + upstream), work,
["-fsanitize=undefined", "-fsanitize-undefined-trap-on-error"])
compile_run("ems", (HERE / "ems.c").read_text().replace("/* SDK_FUNCTIONS */",
extract(texts["mbedtls_ssl_tls"], "ssl_compute_master")), work,
["-fsanitize=undefined", "-fsanitize-undefined-trap-on-error"])
x509 = texts["mbedtls_x509_create"]
functions = x509[x509.index("typedef struct {"):x509.index("int mbedtls_x509_string_to_names(")]
functions += extract(x509, "mbedtls_x509_string_to_names")
compile_run("x509", (HERE / "x509.c").read_text().replace("/* SDK_FUNCTIONS */", functions), work,
["-I", str(idf / "components/mbedtls/mbedtls/include"),
"-fsanitize=undefined", "-fsanitize-undefined-trap-on-error"])
def compile_run(name, source, work, flags=()):
@@ -175,41 +204,86 @@ def compile_run(name, source, work, flags=()):
def cmake_fixture_tests(idf, work):
# Use real component inputs with mock IDF target discovery. No SDK compilation.
fixture = work / "cmake_fixture"; fixture.mkdir()
installed_idf = idf
idf = fixture / "idf"
for rel in ["components/esp_common/include/esp_idf_version.h"] + [e.source for e in sdk.ENTRIES if e.root == "idf"]:
dest = idf / rel; dest.parent.mkdir(parents=True, exist_ok=True)
shutil.copyfile(installed_idf / rel, dest)
nested_dir = idf / "components/mbedtls/mbedtls/library"
nested_lines = []
lines = ["cmake_minimum_required(VERSION 3.18)", "project(security_fixture C)",
'set(CMAKE_EXPORT_COMPILE_COMMANDS ON)',
f'set(TEST_IDF "{idf}")',
'function(idf_build_get_property out property)',
' set(${out} "${TEST_IDF}" PARENT_SCOPE)', 'endfunction()',
'function(idf_component_get_property out component property)',
' set(${out} "test_${component}" PARENT_SCOPE)', 'endfunction()']
targets = set()
for e in sdk.ENTRIES:
if e.root == "project":
copied = fixture / e.source
copied.parent.mkdir(parents=True, exist_ok=True)
shutil.copyfile(source_path(e, idf), copied)
lines += [f'add_library(test_{e.component} STATIC "{source_path(e, idf, fixture)}")']
target = e.target or f"test_{e.component}"
owner_lines = nested_lines if e.target else lines
registered_source = Path(e.source).name if e.target else source_path(e, idf, fixture)
if target not in targets:
owner_lines += [f'add_library({target} STATIC "{registered_source}")']
targets.add(target)
else:
owner_lines += [f'target_sources({target} PRIVATE "{source_path(e, idf, fixture)}")']
lines += ['add_library(test_mbedtls INTERFACE)', f'add_subdirectory("{nested_dir}" nested)']
(nested_dir / "CMakeLists.txt").write_text(
'if(NOT TEST_NESTED_MISSING AND NOT TEST_NESTED_OWNER)\n' + "\n".join(nested_lines) + '\nendif()\n')
lines += ['if(TEST_NESTED_OWNER)',
f'add_library(mbedtls STATIC "{source_path(sdk.ENTRIES[1], idf)}")', 'endif()']
for target in ("mbedtls", "mbedx509"):
entry = next(e for e in sdk.ENTRIES if e.target == target)
lines += [f'if(TEST_{target}_MISSING_SOURCE)', f'set_property(TARGET {target} PROPERTY SOURCES missing.c)', 'endif()',
f'if(TEST_{target}_DUPLICATE)', f'set_property(TARGET {target} APPEND PROPERTY SOURCES "{source_path(entry, idf)}")', 'endif()']
lines += ['if(TEST_MISSING)', f'set_property(TARGET test_{sdk.ENTRIES[0].component} PROPERTY SOURCES missing.c)', 'endif()',
'if(TEST_AMBIGUOUS)', f'set_property(TARGET test_{sdk.ENTRIES[0].component} APPEND PROPERTY SOURCES "{source_path(sdk.ENTRIES[0], idf, fixture)}")', 'endif()',
'if(TEST_TARGET_MISSING)', 'function(idf_component_get_property out component property)',
'set(${out} nonexistent PARENT_SCOPE)', 'endfunction()', 'endif()']
for e in sdk.ENTRIES:
lines += [f'set_source_files_properties("{source_path(e, idf, fixture)}" PROPERTIES COMPILE_FLAGS "-DSOURCE_FLAG" COMPILE_DEFINITIONS "SOURCE_DEFINE" COMPILE_OPTIONS "-fno-common")']
directory = f' DIRECTORY "{nested_dir}"' if e.target else ''
lines += [f'set_source_files_properties("{source_path(e, idf, fixture)}"{directory} PROPERTIES COMPILE_FLAGS "-DSOURCE_FLAG" COMPILE_DEFINITIONS "SOURCE_DEFINE" COMPILE_OPTIONS "-fno-common")']
lines += [f'include("{ROOT / "cmake/security_overrides.cmake"}")']
for e in sdk.ENTRIES:
lines += [f'file(GENERATE OUTPUT "${{CMAKE_BINARY_DIR}}/{e.name}.sources" CONTENT "$<TARGET_PROPERTY:test_{e.component},SOURCES>")',
f'get_property(flags SOURCE "${{SAK_SECURITY_{e.name}_GENERATED}}" PROPERTY COMPILE_FLAGS)',
target = e.target or f"test_{e.component}"
directory = f' DIRECTORY "{nested_dir}"' if e.target else ''
lines += [f'file(GENERATE OUTPUT "${{CMAKE_BINARY_DIR}}/{e.name}.sources" CONTENT "$<TARGET_PROPERTY:{target},SOURCES>")',
f'get_property(flags SOURCE "${{SAK_SECURITY_{e.name}_GENERATED}}"{directory} PROPERTY COMPILE_FLAGS)',
'if(NOT flags STREQUAL "-DSOURCE_FLAG")', 'message(FATAL_ERROR "lost compile flags")', 'endif()',
f'get_property(inc SOURCE "${{SAK_SECURITY_{e.name}_GENERATED}}" PROPERTY INCLUDE_DIRECTORIES)',
f'get_property(inc SOURCE "${{SAK_SECURITY_{e.name}_GENERATED}}"{directory} PROPERTY INCLUDE_DIRECTORIES)',
f'if(NOT inc MATCHES "{source_path(e, idf, fixture).parent}")', 'message(FATAL_ERROR "lost original quoted include directory")', 'endif()']
(fixture / "CMakeLists.txt").write_text("\n".join(lines) + "\n")
build = work / "cmake_good"
run(["cmake", "-G", "Ninja", "-S", fixture, "-B", build])
for e in sdk.ENTRIES:
source = (build / (e.name + ".sources")).read_text()
assert source == str(build / "security_overrides" / e.name / Path(e.source).name)
assert source.split(';').count(str(build / "security_overrides" / e.name / Path(e.source).name)) == 1
assert str(source_path(e, idf, fixture)) not in source
build_registration(build, idf, fixture)
commands = json.loads((build / "compile_commands.json").read_text())
for e in sdk.ENTRIES:
generated = str(build / "security_overrides" / e.name / Path(e.source).name)
matches = [c for c in commands if c["file"] == generated]
assert len(matches) == 1, (e.name, matches)
for option in ("-DSOURCE_FLAG", "-DSOURCE_DEFINE", "-fno-common",
str(source_path(e, idf, fixture).parent)):
assert option in matches[0]["command"], (e.name, option, matches)
ninja = (build / "build.ninja").read_text()
for path in [ROOT / "tools/security_overrides.py", idf / "components/esp_common/include/esp_idf_version.h"] + [source_path(e, idf, fixture) for e in sdk.ENTRIES]:
assert str(path) in next(line for line in ninja.splitlines() if ": RERUN_CMAKE" in line), path
for flag, phrase in (("TEST_MISSING", "found 0"), ("TEST_AMBIGUOUS", "found 2"), ("TEST_TARGET_MISSING", "missing component target")):
for flag, phrase in (("TEST_MISSING", "found 0"), ("TEST_AMBIGUOUS", "found 2"),
("TEST_TARGET_MISSING", "missing component target"),
("TEST_NESTED_MISSING", "missing nested target"),
("TEST_NESTED_OWNER", "unexpected nested target owner"),
("TEST_mbedtls_MISSING_SOURCE", "found 0"),
("TEST_mbedtls_DUPLICATE", "found 2"),
("TEST_mbedx509_MISSING_SOURCE", "found 0"),
("TEST_mbedx509_DUPLICATE", "found 2")):
output = run(["cmake", "-G", "Ninja", "-S", fixture, "-B", work / flag, "-D" + flag + "=ON"], ok=False)
assert phrase in output, output
print("CMake actual include: exact target replacement/properties/reconfigure/fail-closed matrix PASS")
@@ -267,16 +341,19 @@ def extension_fixture_tests(idf, work):
print("Extension mapping + child relative source/includes/flags real compile + automatic mismatch rejection PASS")
def build_registration(build, idf):
def build_registration(build, idf, project=ROOT):
ninja = (build / "build.ninja").read_text()
compile_lines = [line for line in ninja.splitlines() if ": C_COMPILER" in line]
for e in sdk.ENTRIES:
generated = build / "security_overrides" / e.name / Path(e.source).name
matches = [line for line in compile_lines if str(generated) in line]
assert len(matches) == 1, (e.name, matches)
assert not any(str(source_path(e, idf)) in line for line in compile_lines), e.name
assert generated.read_bytes() == sdk.render_entry(e, {"idf": idf, "project": ROOT})[1]
print("Real IDF Ninja registration: each generated source once, originals absent, bytes verified PASS")
assert not any(str(source_path(e, idf, project)) in line for line in compile_lines), e.name
target = e.target or (f"test_{e.component}" if project != ROOT else f"__idf_{e.component}")
assert f"CMakeFiles/{target}.dir/" in matches[0], (e.name, matches)
assert generated.read_bytes() == sdk.render_entry(e, {"idf": idf, "project": project})[1]
kind = "Real IDF" if project == ROOT else "CMake fixture"
print(f"{kind} Ninja registration: generated inputs once on exact owner, originals absent, bytes verified PASS")
def main():
+76
View File
@@ -0,0 +1,76 @@
/* SPDX-License-Identifier: GPL-3.0-only */
#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "mbedtls/x509.h"
#include "mbedtls/asn1write.h"
#include "mbedtls/oid.h"
static unsigned allocations, fail_at, live;
static void *test_calloc(size_t n, size_t size)
{
if (++allocations == fail_at) return NULL;
void *p = calloc(n, size); assert(p); live++; return p;
}
static void test_free(void *p) { if (p) { assert(live); live--; free(p); } }
#define mbedtls_calloc test_calloc
#define mbedtls_free test_free
void mbedtls_asn1_free_named_data_list(mbedtls_asn1_named_data **head)
{
while (*head) {
mbedtls_asn1_named_data *next = (*head)->next;
test_free((*head)->oid.p); test_free((*head)->val.p); test_free(*head);
*head = next;
}
}
/* Storage double: preserve the real parser's allocation/copy/free sequence;
* unrelated repeated-OID replacement behavior is deliberately not modeled. */
mbedtls_asn1_named_data *mbedtls_asn1_store_named_data(mbedtls_asn1_named_data **head,
const char *oid, size_t oid_len, const unsigned char *val, size_t val_len)
{
mbedtls_asn1_named_data *node = test_calloc(1, sizeof(*node));
if (!node) return NULL;
node->oid.p = test_calloc(1, oid_len);
node->val.p = test_calloc(1, val_len);
if (!node->oid.p || !node->val.p) {
test_free(node->oid.p); test_free(node->val.p); test_free(node); return NULL;
}
memcpy(node->oid.p, oid, oid_len); node->oid.len = oid_len;
memcpy(node->val.p, val, val_len); node->val.len = val_len;
node->next = *head; *head = node; return node;
}
int mbedtls_oid_from_numeric_string(mbedtls_asn1_buf *oid, const char *p, size_t n)
{ assert(!"numeric OID outside this focused test"); return -1; }
int mbedtls_asn1_get_len(unsigned char **p, const unsigned char *end, size_t *len)
{ assert(!"hex DER outside this focused test"); return -1; }
/* SDK_FUNCTIONS */
int main(void)
{
/* Subject and issuer each call this same parser; fail each allocation in
* their separate lists, then destroy partial lists and retry from scratch. */
for (unsigned failure = 1; failure <= 8; failure++) {
mbedtls_asn1_named_data *subject = NULL, *issuer = NULL;
allocations = 0; fail_at = failure;
int ret = mbedtls_x509_string_to_names(&subject, "CN=ESP32 SAK ABCDEF");
if (!ret) ret = mbedtls_x509_string_to_names(&issuer, "CN=ESP32 SAK ABCDEF");
assert(ret == MBEDTLS_ERR_X509_ALLOC_FAILED);
mbedtls_asn1_free_named_data_list(&subject);
mbedtls_asn1_free_named_data_list(&issuer);
assert(live == 0);
allocations = 0; fail_at = 0;
assert(mbedtls_x509_string_to_names(&subject, "CN=ESP32 SAK ABCDEF") == 0);
assert(mbedtls_x509_string_to_names(&issuer, "CN=ESP32 SAK ABCDEF") == 0);
assert(subject->val.len == strlen("ESP32 SAK ABCDEF"));
assert(memcmp(subject->val.p, "ESP32 SAK ABCDEF", subject->val.len) == 0);
assert(issuer->val.tag == MBEDTLS_ASN1_UTF8_STRING);
mbedtls_asn1_free_named_data_list(&subject);
mbedtls_asn1_free_named_data_list(&issuer);
assert(live == 0);
}
mbedtls_asn1_named_data *head = NULL;
allocations = 0; fail_at = 5; /* Second known-attribute OID, after a live node. */
assert(mbedtls_x509_string_to_names(&head, "CN=first,O=second") == MBEDTLS_ERR_X509_ALLOC_FAILED);
assert(head && head->val.len == 5);
mbedtls_asn1_free_named_data_list(&head); assert(live == 0);
puts("X509 extracted name parser/helpers: subject/issuer OOM, partial-list cleanup, successful retry PASS");
}
+97
View File
@@ -0,0 +1,97 @@
# Bounded CVE-2025-12888 mitigation
Scope: project-owned build configuration only, retaining wolfSSL 5.8.2~1 and
wolfSSH 1.4.20 pins and unmodified managed sources. This is not an upstream
upgrade, blanket security clearance, or mitigation of other listed advisories.
## Upstream and installed evidence
On 2026-09-15, inspected official
[PR9275 files](https://api.github.com/repos/wolfSSL/wolfssl/pulls/9275/files)
([PR](https://github.com/wolfSSL/wolfssl/pull/9275), head reported by the files
API: `c161cbd9f3fa1247382bb5b6269c7379222cabf5`). Its `settings.h` patch
selects `CURVE25519_SMALL`, `ED25519_SMALL`, `CURVE448_SMALL`, and `ED448_SMALL`
under `__xtensa__`: Xtensa compilers have generated non-constant-time assembly
from the fast C implementation; upstream says the small implementation is not
known to have those issues. This is upstream mitigation guidance, not proof of
constant-time execution on our compiler/device.
Installed `include/user_settings.h` enables X25519 and Ed25519. Installed
`wolfssl/wolfcrypt/settings.h` automatically enables X25519 blinding only for
non-small math; `wolfcrypt/src/curve25519.c` rejects blinding with small math.
`fe_low_mem.c` and `ge_low_mem.c` provide the small implementations and already
have entries in the production compilation database. Small flags change public
key layout/signatures: never mix old library objects with newly compiled callers.
Root `CMakeLists.txt` sets both small flags before component processing, alongside
the existing global crypto controls. `cmake/wolf_crypto_policy.cmake` attaches a
forced-include resolved-settings guard to wolfSSL with PUBLIC propagation to its
consumers, including wolfSSH and application code. The guard rejects missing
algorithms/small flags, incompatible blinding, and future 448 enablement pending
explicit review. No blinding-disable macro or vendor source patch is needed.
RNG callback and software AES/SHA settings remain unchanged.
## Commands
From the repository root, after the parent regenerates/builds the firmware:
```sh
python3 tests/wolf_crypto_policy/run.py
```
Optional explicit database:
```sh
python3 tests/wolf_crypto_policy/run.py --compile-commands .pio/build/esp32-s3-devkitc-1-n16r8/compile_commands.json
```
Strict mode requires the actual compile commands to carry the policy guard and
uses their actual compiler, include paths and definitions without adding small
flags. Missing/ambiguous entries, absent policy, wrong architecture, incompatible
macros, compiler errors and failed vectors fail the test. It preprocesses and
syntax-checks ten translation units: Curve25519, Ed25519, fast and small field/group
math, wolfSSH `ssh.c`, generated wolfSSH `internal.c`, application transport and
security. Two additional actual-settings checks remove each small flag and must
fail. It does not modify generated sources or compile databases.
Before the parent reconfigures, explicitly test the candidate using old commands:
```sh
python3 tests/wolf_crypto_policy/run.py --candidate
```
This injects the two small flags and the guard and labels its output **CANDIDATE
replay**, not production configuration evidence. It does not run CMake/PlatformIO.
Host-only subset:
```sh
python3 tests/wolf_crypto_policy/run.py --host-only
```
All modes run eight guard matrix cases and compile the installed vendor small
implementations into a temporary host executable: RFC7748 section 6.1 X25519
shared secret, RFC8032 section 7.1 test 1 Ed25519 empty-message verification, and
rejection of a corrupted signature. Host settings are deliberately minimal,
with streaming verification enabled and unused functions garbage-collected;
they are not the ESP-IDF runtime/entropy/hardware configuration. No synthetic
implementation substitutes for the tested arithmetic. Requirements: Python 3,
`cc`/linker, installed managed component; target checks also require the existing
Xtensa toolchain, generated headers and compile database. Commands are bounded;
temporary outputs are removed automatically.
## Validation and remaining gates
Implemented validation: candidate replay passed all ten macro/syntax checks,
eight guard cases, two real-settings rejection cases, and the three host vector
checks. Initial host harness compilation exposed a disabled SHA256 declaration
dependency and omitted small-math source files; the harness was corrected to use
the installed small source files explicitly.
The parent must run the normal full build and then strict mode above. A build was
explicitly not run for this task. Existing compile-database success alone would
not prove the linked/flashed image matches it. No device operations were run.
Still required: target SSH X25519 negotiation, Ed25519 authentication, rekey,
combined service load, stack/heap reserves and handshake latency/deadline checks.
Small implementations may reduce performance; no target timing, side-channel
measurement, interoperability or resource claim is made. Host vectors are narrow
correctness checks, not exhaustive cryptographic validation.
+164
View File
@@ -0,0 +1,164 @@
#!/usr/bin/env python3
"""Offline compile-profile regression; never invokes PlatformIO or a device."""
import argparse
import json
import os
from pathlib import Path
import re
import shlex
import subprocess
import tempfile
ROOT = Path(__file__).resolve().parents[2]
HERE = Path(__file__).resolve().parent
GUARD = ROOT / 'cmake/wolf_crypto_policy.h'
VENDOR = ROOT / 'managed_components/wolfssl__wolfssl'
ENV = {**os.environ, 'CCACHE_DISABLE': '1'}
SMALL = ('CURVE25519_SMALL', 'ED25519_SMALL')
def run(args, cwd=ROOT, **kw):
return subprocess.run(args, cwd=cwd, env=ENV, timeout=60,
capture_output=True, text=True, **kw)
def require(result):
if result.returncode:
raise RuntimeError(result.stderr)
return result.stdout
def clean(entry):
args = entry.get('arguments') or shlex.split(entry['command'])
result = []
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'):
result.append(arg)
return result
def matrix():
with tempfile.TemporaryDirectory(prefix='wolf-policy-') as tmp:
tmp = Path(tmp)
settings = tmp / 'wolfssl/wolfcrypt/settings.h'
settings.parent.mkdir(parents=True)
settings.write_text('/* Resolved settings supplied by matrix. */\n')
base = ['HAVE_CURVE25519', 'HAVE_ED25519', *SMALL]
cases = [('valid', base, True)]
cases += [(f'missing {m}', [x for x in base if x != m], False) for m in base]
cases += [(m, base + [m], False) for m in
('WOLFSSL_CURVE25519_BLINDING', 'HAVE_CURVE448', 'HAVE_ED448')]
for label, defines, good in cases:
p = run(['cc', '-x', 'c', '-fsyntax-only', '-I' + str(tmp),
'-include', str(GUARD), *['-D' + x for x in defines], '-'], input='')
if (p.returncode == 0) != good or (not good and 'wolf crypto policy:' not in p.stderr):
raise RuntimeError(f'guard matrix failed: {label}: {p.stderr}')
print(f'PASS: {len(cases)} fail-closed guard cases')
def profiles(database, candidate):
entries = json.loads(database.read_text())
suffixes = ('wolfcrypt/src/curve25519.c', 'wolfcrypt/src/ed25519.c',
'wolfcrypt/src/fe_operations.c', 'wolfcrypt/src/ge_operations.c',
'wolfcrypt/src/fe_low_mem.c', 'wolfcrypt/src/ge_low_mem.c',
'wolfssl__wolfssh/src/ssh.c',
'security_overrides/wolfssh_internal/internal.c',
'src/ssh_transport.c', 'src/ssh_security.c')
for suffix in suffixes:
matches = [e for e in entries if e['file'].endswith('/' + suffix)]
if len(matches) != 1:
raise RuntimeError(f'expected one compile entry for {suffix}: {len(matches)}')
entry = matches[0]
command = clean(entry)
if candidate:
command += ['-D' + x for x in SMALL] + ['-include', str(GUARD)]
elif not any('wolf_crypto_policy.h' in x for x in command):
raise RuntimeError(f'{suffix}: missing production guard; parent must reconfigure/build')
text = require(run(command + ['-E', '-dM'], cwd=entry['directory']))
macros = dict(re.findall(r'^#define (\w+)(?: (.*))?$', text, re.M))
for name in (*SMALL, 'HAVE_CURVE25519', 'HAVE_ED25519',
'WC_RNG_SEED_CB', 'WOLFSSL_ED25519_STREAMING_VERIFY',
'NO_WOLFSSL_ESP32_CRYPT_AES', 'NO_WOLFSSL_ESP32_CRYPT_HASH'):
if name not in macros:
raise RuntimeError(f'{suffix}: missing resolved {name}')
if not any(x in macros for x in ('__XTENSA__', '__xtensa__')):
raise RuntimeError('expected actual Xtensa compiler')
for name in ('WOLFSSL_CURVE25519_BLINDING', 'HAVE_CURVE448', 'HAVE_ED448'):
if name in macros:
raise RuntimeError(f'{suffix}: unexpected {name}')
require(run(command + ['-fsyntax-only'], cwd=entry['directory']))
print(f'PASS: {"CANDIDATE replay" if candidate else "production"} macros + syntax: {suffix}')
# Exercise failures with the real installed settings, not only fake headers.
entry = matches[0]
base = [x for x in clean(entry) if x not in ['-D' + m for m in SMALL]
and 'wolf_crypto_policy.h' not in x]
for missing in SMALL:
command = base + ['-D' + x for x in SMALL if x != missing]
command += ['-U' + missing, '-include', str(GUARD), '-E']
p = run(command, cwd=entry['directory'])
if p.returncode == 0 or 'wolf crypto policy:' not in p.stderr:
raise RuntimeError(f'real settings accepted missing {missing}: {p.stderr}')
print('PASS: real target settings reject either missing small flag')
def vectors():
with tempfile.TemporaryDirectory(prefix='wolf-vectors-') as tmp:
tmp = Path(tmp)
(tmp / 'user_settings.h').write_text('''
#define WOLFCRYPT_ONLY
#define NO_ASN
#define NO_RSA
#define NO_DH
#define NO_DSA
#define NO_AES
#define NO_DES3
#define NO_RC4
#define NO_MD4
#define NO_MD5
#define NO_SHA
#define NO_HMAC
#define WC_NO_RNG
#define NO_FILESYSTEM
#define NO_WRITEV
#define NO_DEV_RANDOM
#define NO_MAIN_DRIVER
#define HAVE_CURVE25519
#define HAVE_ED25519
#define CURVE25519_SMALL
#define ED25519_SMALL
#define WOLFSSL_SHA512
#define WOLFSSL_ED25519_STREAMING_VERIFY
''')
sources = ['curve25519.c', 'ed25519.c', 'fe_operations.c',
'ge_operations.c', 'fe_low_mem.c', 'ge_low_mem.c', 'sha512.c']
require(run(['cc', '-std=c99', '-O2', '-DWOLFSSL_USER_SETTINGS',
'-I' + str(tmp), '-I' + str(VENDOR), '-include', str(GUARD),
'-ffunction-sections', '-fdata-sections',
str(HERE / 'vectors.c'),
*[str(VENDOR / 'wolfcrypt/src' / s) for s in sources],
'-Wl,--gc-sections', '-o', str(tmp / 'vectors')]))
print(require(run([str(tmp / 'vectors')])).strip())
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--compile-commands', type=Path,
default=ROOT / '.pio/build/esp32-s3-devkitc-1-n16r8/compile_commands.json')
parser.add_argument('--candidate', action='store_true',
help='inject proposed flags/guard into old commands; NOT production evidence')
parser.add_argument('--host-only', action='store_true')
args = parser.parse_args()
matrix()
vectors()
if not args.host_only:
profiles(args.compile_commands, args.candidate)
if __name__ == '__main__':
main()
+56
View File
@@ -0,0 +1,56 @@
/* SPDX-License-Identifier: GPL-3.0-only */
#include <stdio.h>
#include <string.h>
#include <wolfssl/wolfcrypt/curve25519.h>
#include <wolfssl/wolfcrypt/ed25519.h>
#define CHECK(x) do { if (!(x)) { \
fprintf(stderr, "vector failure at line %d: %s\n", __LINE__, #x); return 1; \
} } while (0)
static void unhex(const char *hex, unsigned char *out, unsigned int size)
{
for (unsigned int i = 0; i < size; ++i) {
unsigned int value;
if (sscanf(hex + 2 * i, "%2x", &value) != 1) return;
out[i] = (unsigned char)value;
}
}
int main(void)
{
/* RFC 7748 section 6.1: Alice private / Bob public / shared secret. */
unsigned char secret[32], peer[32], expected[32], result[32];
curve25519_key alice, bob;
word32 size = sizeof(result);
unhex("77076d0a7318a57d3c16c17251b26645df4c2f87ebc0992ab177fba51db92c2a", secret, 32);
unhex("de9edb7d7b7dc1b4d35b61c2ece435373f8343c85b78674dadfc7e146f882b4f", peer, 32);
unhex("4a5d9d5ba4ce2de1728e3bf480350f25e07e21c947d19e3376f09b3c1e161742", expected, 32);
CHECK(wc_curve25519_init(&alice) == 0);
CHECK(wc_curve25519_init(&bob) == 0);
CHECK(wc_curve25519_import_private_ex(secret, 32, &alice, EC25519_LITTLE_ENDIAN) == 0);
CHECK(wc_curve25519_import_public_ex(peer, 32, &bob, EC25519_LITTLE_ENDIAN) == 0);
CHECK(wc_curve25519_shared_secret_ex(&alice, &bob, result, &size, EC25519_LITTLE_ENDIAN) == 0);
CHECK(size == 32 && memcmp(result, expected, 32) == 0);
wc_curve25519_free(&alice);
wc_curve25519_free(&bob);
/* RFC 8032 section 7.1 test 1: empty-message signature. */
ed25519_key key;
unsigned char public_key[32], signature[64];
int valid = 0;
unhex("d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a", public_key, 32);
unhex("e5564300c360ac729086e2cc806e828a84877f1eb8e5d974d873e065224901555f"
"b8821590a33bacc61e39701cf9b46bd25bf5f0595bbe24655141438e7a100b", signature, 64);
CHECK(wc_ed25519_init(&key) == 0);
CHECK(wc_ed25519_import_public(public_key, 32, &key) == 0);
CHECK(wc_ed25519_verify_msg(signature, 64, (const unsigned char *)"", 0, &valid, &key) == 0);
CHECK(valid == 1);
signature[0] ^= 1;
valid = 0;
(void)wc_ed25519_verify_msg(signature, 64, (const unsigned char *)"", 0, &valid, &key);
CHECK(valid == 0);
wc_ed25519_free(&key);
puts("PASS: host installed small math RFC7748 X25519 / RFC8032 Ed25519 + bad signature");
return 0;
}
+105 -2
View File
@@ -36,6 +36,15 @@ class Entry:
source: str
sha256: str
edits: tuple[Edit, ...]
target: str = "" # Explicit nested library; empty means IDF COMPONENT_LIB.
MODIFICATION_NOTICE = """/* Modified by the ESP32_serial_swiss_army_knife project on 2026-09-15.
* Generated security-corrected source; edits are maintained in
* tools/security_overrides.py. Do not edit this generated copy.
* Upstream copyright and license notices are retained below.
*/
"""
WIPE = """/* Retired secret-bearing storage must not survive allocator reuse. */
@@ -103,7 +112,98 @@ TLS_POLICY = """ /* mbedTLS retains this pointer: it must outlive every serve
"""
# Official patches (TLS 1.2 hunk only for EMS):
# https://github.com/espressif/esp-idf/commit/d51b1076092487e533eadf8b48c9c8579d3a6712.patch
# https://github.com/Mbed-TLS/mbedtls/commit/f595df4569c1a1650ad9d077e2f2e819e9f1dddb.patch
# https://github.com/Mbed-TLS/mbedtls/commit/bfaf4a47fd33da860796feaba6235847acb71127.patch
# DHCP uses equivalent remaining-length checks to avoid forming pointers beyond
# the input object. Keep original upstream notices verbatim, rather than changing
# their copyright year; the central project modification notice is separate.
ENTRIES = (
Entry("dhcpserver", "lwip", "idf",
"components/lwip/apps/dhcpserver/dhcpserver.c",
"953f46189bc64680ea5fa761e75511fadb3aebf698a0d9dff251d77166d78b80", (
Edit("#define DHCP_OPTION_SUBNET_MASK 1",
"#define DHCP_OPTION_PAD 0\n#define DHCP_OPTION_SUBNET_MASK 1"),
Edit(" bool is_dhcp_parse_end = false;\n", ""),
Edit(" switch ((s16_t) *optptr) {", """ if (*optptr == DHCP_OPTION_PAD) {
optptr++;
continue;
}
if (*optptr == DHCP_OPTION_END) {
break;
}
if (end - optptr < 2) {
break;
}
u8_t opt_len = optptr[1];
if (opt_len > end - optptr - 2) {
break;
}
switch ((s16_t) *optptr) {"""),
Edit(" type = *(optptr + 2);", """ if (opt_len >= 1) {
type = optptr[2];
}"""),
Edit(""" if (memcmp((char *) &client.addr, (char *) optptr + 2, 4) == 0) {
#if DHCPS_DEBUG
DHCPS_LOG("dhcps: DHCP_OPTION_REQ_IPADDR = 0 ok\\n");
#endif
s.state = DHCPS_STATE_ACK;
} else {
#if DHCPS_DEBUG
DHCPS_LOG("dhcps: DHCP_OPTION_REQ_IPADDR != 0 err\\n");
#endif
s.state = DHCPS_STATE_NAK;
}""", """ if (opt_len >= 4) {
if (memcmp((char *) &client.addr, (char *) optptr + 2, 4) == 0) {
#if DHCPS_DEBUG
DHCPS_LOG("dhcps: DHCP_OPTION_REQ_IPADDR = 0 ok\\n");
#endif
s.state = DHCPS_STATE_ACK;
} else {
#if DHCPS_DEBUG
DHCPS_LOG("dhcps: DHCP_OPTION_REQ_IPADDR != 0 err\\n");
#endif
s.state = DHCPS_STATE_NAK;
}
}"""),
Edit("""
case DHCP_OPTION_END: {
is_dhcp_parse_end = true;
}
break;
}
if (is_dhcp_parse_end) {
break;
}
optptr += optptr[1] + 2;""", """
}
optptr += opt_len + 2;"""),
)),
Entry("mbedtls_ssl_tls", "mbedtls", "idf",
"components/mbedtls/mbedtls/library/ssl_tls.c",
"b726c0c55bc5f32255f129d55f9f2fface85ce83de90a2d16c9017b93b738bff", (
Edit(' MBEDTLS_SSL_DEBUG_RET(1, "calc_verify", ret);\n',
' MBEDTLS_SSL_DEBUG_RET(1, "calc_verify", ret);\n return ret;\n'),
), target="mbedtls"),
Entry("mbedtls_x509_create", "mbedtls", "idf",
"components/mbedtls/mbedtls/library/x509_create.c",
"fd399239aee30384786a19b47bfe5dd22b979d5d89bb38f29f0c82a3d81daaf7", (
Edit(" oid.p = mbedtls_calloc(1, oid.len);\n",
""" oid.p = mbedtls_calloc(1, oid.len);
if (oid.p == NULL) {
return MBEDTLS_ERR_X509_ALLOC_FAILED;
}
"""),
), target="mbedx509"),
Entry("wolfssh_internal", "wolfssl__wolfssh", "project",
"managed_components/wolfssl__wolfssh/src/internal.c",
"81ff1f9166708abd5c2911e9fe57c0aee01c88b5d3f68c909ee8a856d37f36a9", (
@@ -283,6 +383,9 @@ def verify_version(idf: Path) -> Path:
def render_entry(entry: Entry, roots: dict[str, Path]) -> tuple[Path, bytes]:
if not all(re.fullmatch(r"[a-zA-Z0-9_-]+", value) for value in (entry.name, entry.component)):
raise OverrideError("invalid entry name/component")
if entry.target and (entry.component != "mbedtls" or entry.target not in
{"mbedtls", "mbedx509", "mbedcrypto"}):
raise OverrideError("invalid nested target selection")
root = roots[entry.root].resolve()
source = (root / entry.source).resolve()
if not source.is_relative_to(root):
@@ -291,7 +394,7 @@ def render_entry(entry: Entry, roots: dict[str, Path]) -> tuple[Path, bytes]:
actual = hashlib.sha256(raw).hexdigest()
if actual != entry.sha256:
raise OverrideError(f"{entry.name}: SHA256 mismatch for {source}: expected {entry.sha256}, got {actual}; reaudit, do not repin blindly")
return source, apply_edits(raw.decode("utf-8"), entry.edits).encode("utf-8")
return source, (MODIFICATION_NOTICE + apply_edits(raw.decode("utf-8"), entry.edits)).encode("utf-8")
def write_if_changed(path: Path, data: bytes) -> bool:
@@ -341,7 +444,7 @@ def generate(idf: Path, project: Path, binary: Path, entries: tuple[Entry, ...]
"set(SAK_SECURITY_OVERRIDE_IDS " + " ".join(names) + ")",
"set(SAK_SECURITY_VERSION_HEADER " + cmake_quote(str(version)) + ")"]
for entry, source, target, _ in rendered:
for key, value in (("COMPONENT", entry.component), ("ORIGINAL", str(source)),
for key, value in (("COMPONENT", entry.component), ("TARGET", entry.target), ("ORIGINAL", str(source)),
("GENERATED", str(target)), ("SHA256", entry.sha256)):
lines.append(f"set(SAK_SECURITY_{entry.name}_{key} {cmake_quote(value)})")
manifest = output / "manifest.cmake"