Add Phase 9C security hardening

Generate exact-hash SDK source overrides without modifying dependencies.
Harden
SSH allocation and algorithm policy, tighten web authentication cleanup,
and add
focused host contract tests and documentation.
This commit is contained in:
2026-09-15 22:12:57 +02:00
parent 751dfb9ddb
commit cdc9c7335a
41 changed files with 3597 additions and 89 deletions
+134
View File
@@ -0,0 +1,134 @@
# Pinned SDK security overrides
The root `CMakeLists.txt` includes `cmake/security_overrides.cmake` **after**
`project()`. No component/vendor file is edited, and no global crypto feature or
client ciphersuite setting is changed.
## Build contract
`tools/security_overrides.py` requires the installed ESP-IDF version header to
identify **5.5.0**, and checks each complete original source against its reviewed
SHA256. Every text substitution must match **exactly once**. The entire input
plan is validated before any output is written. A changed SDK, missing source,
ambiguous edit, duplicate source, or missing/ambiguous component target fails
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>`.
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
reconfiguration. The included CMake file is itself an ordinary CMake input.
Requirements: Python 3.9+ and CMake 3.18+ (directory-scoped source properties).
CMake replaces the exact original entry in the existing component's `SOURCES`;
it does not add a second definition or replace the component target. Target
compile settings remain intact. Source compile flags/options/definitions,
per-configuration definitions, source includes and object dependencies are
copied in the target's owning directory. The original C file's directory is
prepended to that source's include search path, preserving quoted local headers.
Source generator expressions are rejected rather than guessed through.
### Current corrections
- `esp_https_server:src/https_server.c`: delete TLS if post-handshake transport
allocation fails; destroy the complete secure context if HTTPD start fails;
wipe exactly `serverkey_bytes` before releasing the raw private-key copy.
Failed start restores the original open callback and clears stale transport
context/destructor pointers. Failed stop retains live ownership.
- `esp_http_server:src/httpd_parse.c`: allocate/copy/wipe/free scratch resize,
retaining old storage on failure; wipe current scratch on final cleanup.
Initial reads avoid null-pointer subtraction and preserve a null parser
position until a callback sets it; existing positions relocate with scratch.
Existing shrink/grow behavior and bounds remain. Resizing briefly owns old
plus new allocations; no persistent maximum-size buffer, socket, task or
limit increase. Pending/unread bytes are not erased or drained by the patch.
- `esp-tls:esp_tls_mbedtls.c`: **server-local** static-lifetime allowlist of
`TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256` and
`TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384`, terminated by zero, configured after
server defaults and before setup. Both version limits are TLS 1.2;
renegotiation is disabled (or absent at compile time). Required TLS/ECDHE/
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
original source SHA256 to
`81ff1f9166708abd5c2911e9fe57c0aee01c88b5d3f68c909ee8a856d37f36a9`.
`GetSize()` bounds password and new-password fields before authentication;
malformed parsing cannot reach the auth callback. The checked packet suffix
is wiped before failure responses, preserving the caller's prefix. Pending
asynchronous authentication retains the payload for retry; this is **not**
an async secret-lifetime/wipe guarantee. Generated parser/control-flow tests
live in `tests/wolfssh_auth_contract/`.
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
copy, compiler spill, accelerator register, browser buffer or allocator region.
## Parent extension point
Add an `Entry` to `tools/security_overrides.py:ENTRIES` with:
- unique `name`;
- exact IDF `component` name (used by `idf_component_get_property`);
- `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.
`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
corresponding library-specific feature/behavior tests when extending the registry. Multiple
sources in the same real component are supported by the replacement function.
Do not change a source hash merely to make a new SDK configure. Re-audit ownership,
cleanup, feature resolution and patches against that source revision first.
## Validation
```sh
python3 tests/sdk_security_overrides/run.py
python3 tests/sdk_security_overrides/run.py --build-dir .pio/build/esp32-s3-devkitc-1-n16r8
```
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
Ninja registration: exactly one compilation of each derived source, no original
compilation, and exact generated bytes. It does **not** run a firmware build.
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.
- 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.
The unmodified installed `httpd_stop()` is extracted and separately pinned.
Allocator doubles assert key bytes are zero **before** free, with trailing
canaries to reject over-wiping, and detect leaks/double frees.
- Actual patched scratch helper/read/cleanup functions: grow, shrink, no-change,
null-initial first-read success/failure, nullable parser-position preservation,
resize failure preserving the old pointer, receive errors/timeouts, size bounds, final wipe and
original right-aligned pending-byte behavior. Installed pending/unrecv functions
are separately pinned and executed. This is not a full HTTP parser fuzz test.
- Actual server/client configuration functions with crypto/config doubles:
allowlist order/terminator/static lifetime, defaults failure, PKI failure,
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.
- The actual CMake include under fake IDF target discovery, including missing and
duplicate sources/targets. 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
its hash instead of compiling stale derived code.
No tests here perform real TLS handshakes, network/device operations, allocation
failure on the target, or whole-Phase-9 hardware acceptance.