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:
@@ -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.
|
||||
Reference in New Issue
Block a user