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,143 @@
|
||||
# Security library review — Phase 9C
|
||||
|
||||
## Scope and status
|
||||
|
||||
Bounded implementation/source audit, verified 2026-09-15; not library security certification.
|
||||
Baseline: **ESP-IDF 5.5.0, mbedTLS 3.6.3, wolfSSH 1.4.20, wolfSSL 5.8.2~1**
|
||||
(upstream wolfSSL version macro: 5.8.2). Original dependencies are not upgraded or hand-patched.
|
||||
Versions were checked against installed headers and `src/idf_component.yml`; override hashes
|
||||
were checked against installed originals. Source is authoritative over older integration notes.
|
||||
The reported Phase 9C reviews have no remaining blocking finding; the HTTPD null-initial
|
||||
read finding is fixed and covered by the passing host suite below.
|
||||
Whole-Phase-9 target validation is deferred at the user's request; see [main policy](security_hardening.md).
|
||||
The [main policy](security_hardening.md#validation-gates) records final firmware build/size evidence separately from this source review.
|
||||
|
||||
## Confirmed gaps fixed
|
||||
|
||||
| Boundary / source | Implemented correction |
|
||||
|---|---|
|
||||
| SDK `esp_https_server/src/https_server.c` | Delete TLS when post-handshake transport allocation fails; destroy the complete secure context on HTTPD start failure. Restore the original open callback and clear stale context/destructor pointers. Wipe `serverkey_bytes` before freeing the raw key copy. Failed stop retains live ownership. |
|
||||
| SDK `esp_http_server/src/httpd_parse.c` | Replace scratch realloc with allocate/copy/wipe/free; preserve old pointer/content on allocation failure and wipe current scratch at final cleanup. Preserve pending/unread bytes. Initial reads avoid NULL subtraction and retain a NULL parser position until set; existing positions relocate correctly. |
|
||||
| SDK `esp-tls/esp_tls_mbedtls.c` | Apply the server-local TLS profile below after defaults and before setup; static suite storage, TLS 1.2 minimum/maximum, no renegotiation. Client defaults/caller suites and global crypto features are unchanged. |
|
||||
| wolfSSH `src/internal.c` | Use `GetSize()` bounds for password/new-password fields, reject invalid context/index, and guard authentication dispatch after new-password parse failure. Wipe the checked packet suffix before failure output, preserving the username/service/method prefix needed by the caller. Skip wiping on `WS_AUTH_PENDING` for retry; the project does not return pending. |
|
||||
| `src/ssh_memory.c`, `src/ssh_transport.c` | Register secure wolfSSL/wolfSSH allocation hooks before library allocation; wipe retired heap extents and explicit shrink tails, including allocator rounding. |
|
||||
| `src/ssh_protocol_policy.c`, `src/ssh_transport.c` | Apply all five explicit lists; any setter failure frees the unpublished candidate and returns failure, without default-policy fallback. |
|
||||
| `src/web_cookie_auth.c` | Check exhausted verification budget before body receive/parse, reserve authoritatively after parsing, and shorten JSON/credential lifetime before backend/error output. |
|
||||
|
||||
The three SDK overrides and wolfSSH override are registered in `tools/security_overrides.py`.
|
||||
Root `CMakeLists.txt` includes `cmake/security_overrides.cmake` **after `project()`**;
|
||||
`src/CMakeLists.txt` includes both new SSH modules. No embedded web assets were regenerated.
|
||||
|
||||
## Heap and packet lifetime contract
|
||||
|
||||
`ssh_memory` compile-guards **unpoisoned IDF 5.5.0**: `heap_caps_get_allocated_size()`
|
||||
must return the owned usable extent of a base allocation, not an interior-pointer extent.
|
||||
Allocation remains PSRAM-first with internal fallback; no allocation headers, metadata tables,
|
||||
extra locks or tasks are introduced. Free securely wipes the complete extent before release.
|
||||
Shrink retains the pointer/capacity and wipes the discarded tail; it does not reclaim heap.
|
||||
Growth allocates a replacement, copies the old usable extent, then wipes/frees the old allocation.
|
||||
Failed growth leaves the old allocation and contents unchanged. Growth temporarily needs **old + new**
|
||||
storage, including possible internal fallback. HTTPD resize similarly needs both bounded allocations,
|
||||
but retains its ordinary shrink/grow behavior rather than a permanent maximum-sized scratch buffer.
|
||||
|
||||
These fixes cover specific retired copies, not every secret throughout its lifetime:
|
||||
- Static and still-live library buffers can retain bytes; heap hooks do not intercept in-place compaction.
|
||||
- Packet-suffix wiping is deliberately prefix-preserving and is not an asynchronous-auth wipe guarantee.
|
||||
- Backend-specific spills, stack/register copies, crypto intermediates and accelerator state were not exhaustively audited.
|
||||
- Browser memory, flash history and all allocator regions are not proven clean; do not export raw memory dumps as evidence.
|
||||
|
||||
## Existing cleanup verified, not presumed broken
|
||||
|
||||
Inspection of the installed original sources found existing wipes on the checked normal paths:
|
||||
- wolfSSL `wolfcrypt/src/ecc.c:wc_ecc_free()` calls `mp_forcezero()` for the private scalar;
|
||||
`integer.c` wipes used digits before release, while `tfm.c` delegates to `fp_forcezero()`.
|
||||
- mbedTLS `library/pk_wrap.c:eckey_free_wrap()` delegates to `ecp.c:mbedtls_ecp_keypair_free()`;
|
||||
the private MPI reaches `bignum.c:mbedtls_mpi_free()` and its zeroize-and-free path.
|
||||
- mbedTLS `library/md.c:mbedtls_md_free()` zeroizes/frees HMAC pads and wipes the context.
|
||||
- mbedTLS `library/ssl_tls.c:mbedtls_ssl_free()` zeroizes/frees input/output record buffers;
|
||||
its inspected buffer-resize path also zeroizes retired storage.
|
||||
|
||||
Thus ordinary destructors are **not generally broken across both stacks**. The confirmed gaps above
|
||||
are separate raw-copy, ownership, resize and packet-lifetime issues. IDF dynamic TLS buffers are
|
||||
compile-rejected because their destruction bypasses the inspected upstream record-buffer path;
|
||||
other configurations/backends need their own review, not extrapolation from these observations.
|
||||
|
||||
## Current protocol allowlists and compatibility
|
||||
|
||||
| Layer / setting | Exact current policy |
|
||||
|---|---|
|
||||
| HTTPS versions | TLS 1.2 only; renegotiation disabled or compiled out |
|
||||
| HTTPS suites | `TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256`, `TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384` |
|
||||
| SSH `Kex` | `curve25519-sha256,ecdh-sha2-nistp256` |
|
||||
| SSH `Key` (host identity) | `ecdsa-sha2-nistp256` |
|
||||
| SSH `Cipher` (both directions) | `aes128-gcm@openssh.com,aes256-gcm@openssh.com` |
|
||||
| SSH `Mac` (both advertised directions) | `hmac-sha2-256`; GCM provides the negotiated AEAD integrity |
|
||||
| SSH `KeyAccepted` | `ssh-ed25519,ecdsa-sha2-nistp256` (`server-sig-algs` advertisement only) |
|
||||
| SSH compression | `none` |
|
||||
| SSH authentication | Password or enrolled Ed25519/ECDSA-P256 public key; keyboard-interactive rejected |
|
||||
|
||||
SSH list strings have static lifetime because contexts/sessions borrow their pointers. List setters
|
||||
alone do not validate compiled support; the source/production-feature tests check names, IDs and
|
||||
serialized initial/rekey lists. Enrollment/authorization remains in the user database, not `KeyAccepted`.
|
||||
Legacy CBC/CTR-only SSH clients, excluded KEX/host-key clients, and CBC-only TLS clients cannot connect;
|
||||
TLS clients need TLS 1.2 plus one listed ECDHE-ECDSA GCM suite (TLS-1.3-only also fails).
|
||||
There is no automatic compatibility fallback. Modern-client compatibility is still a live-test gate,
|
||||
not a claim that signature verification, real KEX/rekey or TLS/SSH handshakes were exercised here.
|
||||
|
||||
## Web admission and retained credential/browser policy
|
||||
|
||||
The early quota probe neither consumes attempts nor advances the window. The final post-parse
|
||||
reservation preserves **five password verifications per 60 seconds globally**; malformed requests
|
||||
are not charged. Exhausted requests avoid body receive/parser/KDF and close without draining unread
|
||||
bodies. Challenges remain consumable before this probe: this does **not** establish challenge fairness
|
||||
or prevent global starvation. HTTPS service stop/start resets this window/challenges, unlike SSH's
|
||||
boot-lifetime admission buckets. Epoch/readiness checks fence stale work at both quota boundaries.
|
||||
Raw JSON is wiped after parsing and before KDF; parsed credentials immediately after authentication;
|
||||
denial paths wipe both before error responses. Ordinary final request/token cleanup remains in place.
|
||||
|
||||
`src/user_database.{c,h}` remains unchanged: **12–64 printable ASCII bytes** (`0x20`–`0x7e`),
|
||||
PBKDF2-HMAC-SHA256 with **50,000 iterations**, **16-byte random salt**, **32-byte verifier**.
|
||||
Generated passwords select **24 symbols from 64**, giving **144 bits** with uniform secure randomness.
|
||||
This is a reviewed retained baseline, not a claim that 50,000 iterations meets every current deployment
|
||||
recommendation. Benchmark target verification latency and mixed-load headroom before choosing a new
|
||||
cost; do not blindly increase it. No verifier storage format or key-rotation behavior changes here.
|
||||
|
||||
`src/web_security.c` generates a self-signed **P-256 / ECDSA-SHA256** certificate, non-CA,
|
||||
digital-signature usage, server-auth EKU, device DNS and fixed AP IPv4 SANs, with fixed validity
|
||||
**2025-01-01 through 2049-12-31**. Existing validation checks the key pair, expected fields/SANs and
|
||||
self-signature; this inspection is not a new real-crypto signature-verification test.
|
||||
Compare the certificate SHA-256 fingerprint through trusted UART0 (`web certificate info`) before
|
||||
accepting browser trust; a warning bypass is not verification, nor is arbitrary STA-IP trust solved.
|
||||
Existing persistence/rotation/recovery contracts remain unchanged; NVS is not newly encrypted.
|
||||
|
||||
`src/web_login_ui.c`, `src/web_ui.c` and `src/web_cookie_auth.c` retain CSP, document/auth
|
||||
`Cache-Control: no-store`, and `Secure; HttpOnly; SameSite=Strict` cookies. Static assets retain their
|
||||
separate caching policy. HSTS is deliberately not blindly forced for the self-signed hostname/IP
|
||||
workflow: it is not a substitute for verified certificate trust and may obstruct recovery.
|
||||
|
||||
## Maintenance and evidence
|
||||
|
||||
1. Keep the original SDK/managed sources untouched. Maintain reviewed `Entry` hashes and exact-once
|
||||
edits in `tools/security_overrides.py`; never repin a hash merely to make configuration succeed.
|
||||
2. Re-audit changed source ownership, cleanup, allocator extents, algorithms and resolved features.
|
||||
Full original SHA-256/version mismatch, missing/ambiguous edits or source registration fail closed.
|
||||
3. CMake retains component targets and source properties/quoted-include context, replacing exactly one
|
||||
original compilation per entry. Generator/version/original/derived changes trigger reconfiguration;
|
||||
changed originals fail the hash check. Do not hand-patch SDK files or derived build-tree output.
|
||||
4. Derived full files preserve original copyright/license notices; regenerate through configuration,
|
||||
verify exact generated bytes and single-source registration, then rerun the relevant host contracts.
|
||||
5. Future release/dependency review remains pending: external advisories and license obligations have
|
||||
**not** been reviewed here. No CVE absence, vulnerability completeness or license-compliance claim.
|
||||
|
||||
Verified host commands passed during this documentation audit (prefix `CCACHE_DISABLE=1`):
|
||||
- `python3 tests/sdk_security_overrides/run.py --build-dir .pio/build/esp32-s3-devkitc-1-n16r8`
|
||||
- `python3 tests/ssh_memory/run.py --idf-path /home/mscholz/.platformio/packages/framework-espidf`
|
||||
- `python3 tests/ssh_protocol_policy/run.py`
|
||||
- `python3 tests/wolfssh_auth_contract/run.py`
|
||||
- `python3 tests/web_cookie_auth/run.py`
|
||||
|
||||
These execute actual modules/extracted installed or patched functions with heap, crypto, IO and layout
|
||||
mocks, plus pinned source/production-feature contracts and existing Ninja registration checks.
|
||||
They cover cleanup failures, null-first-read behavior, policy serialization/publication and web quota/wipe
|
||||
ordering; they are not complete parser fuzzing, real signature verification, live handshakes or target tests.
|
||||
No firmware build, upload, erase, raw-dump export or hardware operation was performed for this document.
|
||||
Reference in New Issue
Block a user