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.
12 KiB
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.
The main policy 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()callsmp_forcezero()for the private scalar;integer.cwipes used digits before release, whiletfm.cdelegates tofp_forcezero(). - mbedTLS
library/pk_wrap.c:eckey_free_wrap()delegates toecp.c:mbedtls_ecp_keypair_free(); the private MPI reachesbignum.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
- Keep the original SDK/managed sources untouched. Maintain reviewed
Entryhashes and exact-once edits intools/security_overrides.py; never repin a hash merely to make configuration succeed. - 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.
- 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.
- 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.
- 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-n16r8python3 tests/ssh_memory/run.py --idf-path /home/mscholz/.platformio/packages/framework-espidfpython3 tests/ssh_protocol_policy/run.pypython3 tests/wolfssh_auth_contract/run.pypython3 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.