Files
ESP32_Serial_Swiss_Army_Knife/docs/security_library_review.md
T
Commander1024 cdc9c7335a 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.
2026-09-15 22:12:57 +02:00

12 KiB
Raw Blame History

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() 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: 1264 printable ASCII bytes (0x200x7e), 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.