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
+3 -3
View File
@@ -104,9 +104,9 @@ TinyUSB callbacks enqueue/copy data and state; the transport task owns broker li
### HTTPS, WebSocket, and web serial
`web_server` owns HTTPS on port 443 with a persisted self-signed P-256 identity. `web_serial_transport` mediates two fixed WebSocket slots through the broker; HTTPD owns socket sends/close, the transport task owns broker IO. Four outstanding serial tickets, four cookie sessions, one optional admin WebSocket and six total HTTPD sockets are distinct limits; LRU is disabled. Current handler capacity is 39. Base HTTPS can serve authenticated non-WebSocket routes if optional serial/admin transport initialization fails.
`web_server` owns HTTPS on port 443 with a persisted self-signed P-256 identity. Phase 9C uses exact-hash build-tree SDK corrections for failed-start/post-handshake TLS cleanup, copied-key wiping, HTTPD scratch retirement and TLS1.2 ECDHE-ECDSA AES-GCM-only server policy. Client defaults/global crypto are unchanged. The checked-in override registry plus pinned original, not installed source alone, define compiled behavior. [Source/ownership contract](../security_library_review.md). `web_serial_transport` mediates two fixed WebSocket slots through the broker; HTTPD owns socket sends/close, the transport task owns broker IO. Four outstanding serial tickets, four cookie sessions, one optional admin WebSocket and six total HTTPD sockets are distinct limits; LRU is disabled. Current handler capacity is 39. Base HTTPS can serve authenticated non-WebSocket routes if optional serial/admin transport initialization fails.
Cookie login/logout replaces Basic/cache. Digest-only records carry copied principals, CSRF state, absolute expiry and nonreused originating-session IDs. Strict same-origin/CSRF mutations and session/principal checks gate admission; logout invalidates its session before transport cleanup, account mutations invalidate only the affected account, and ongoing currentness is authoritative. Authentication initialization failure gates HTTPS; failed start/accepted stop wipes records. RNG/SHA/database calls run outside short spinlocks with post-call epoch/identity revalidation. [Authentication contract](../web_administration.md#authentication-and-admission).
Cookie login/logout replaces Basic/cache. Digest-only records carry copied principals, CSRF state, absolute expiry and nonreused originating-session IDs. Strict same-origin/CSRF mutations and session/principal checks gate admission; logout invalidates its session before transport cleanup, account mutations invalidate only the affected account, and ongoing currentness is authoritative. Authentication initialization failure gates HTTPS; failed start/accepted stop wipes records. A non-consuming quota/epoch check rejects exhausted login requests before body receive; verification reservation remains post-parse and only that reservation charges the existing fixed window. RNG/SHA/database calls run outside short spinlocks with post-call epoch/identity revalidation. [Authentication contract](../web_administration.md#authentication-and-admission).
`web_httpd_adapter` is the sole private IDF 5.5.0 boundary for duplicate headers, admission-before-101, consumed-scratch wiping, staged optional URI registration, combined binary sends and owner-only idle sweeps. Re-audit its version guard on SDK upgrades. HTTPD debug logging must not expose headers/tickets. `web_diagnostics` independently observes public post-TLS callbacks using six metadata records and a default-disabled 32-event ring; it cannot see preaccept/in-progress/failed TLS. [Admission diagnostics](../web_admission_diagnostics.md).
@@ -128,7 +128,7 @@ Typed SSH settings use the existing ID dispatcher and original-login result slot
`ssh_transport` uses wolfSSH on port 22 with two fixed session/handshake slots. Initialization calls `wolfSSH_Init()` in the caller before task creation; after that, one owner task pinned to core 1 exclusively owns runtime contexts/sessions and wolfSSH calls. It enforces bounded handshakes, authentication attempts, receive work, and session buffers. Phase 9B adds owner-only boot-lifetime token buckets for handshake admission, password/signed-key checks and unsigned probes; reconnect/service restart/counter clear do not reset them. Rate rejection closes the authenticating connection without sleeping the owner. Global starvation remains a tradeoff; see [admission policy](../security_hardening.md#9b-ssh-admission-and-credential-handling).
Authentication uses user-database passwords or stored Ed25519/ECDSA-P256 public keys. Public-key lookup authorizes a username/key pair, while wolfSSH verifies signed proof of possession. SSH host identity is a separate persisted P-256 key managed by `ssh_security`. A pending-result marker gates exactly-once signed-key completion/currentness; the reviewed wolfSSH version/feature profile is guarded and keyboard-interactive has an explicit rejecting callback (advertisement is not a dispatch filter). Consumed admin staging bytes and retired slots are wiped; this is not a full library-memory wipe guarantee.
Authentication uses user-database passwords or stored Ed25519/ECDSA-P256 public keys. Phase 9C applies explicit GCM/Curve25519/P-256 algorithm lists before context publication; policy failures discard the candidate. A source-pinned parser correction bounds password fields before callbacks and wipes the method payload afterward (synchronous project callbacks). Global wolfSSL memory hooks wipe retired usable allocations; shrink retains capacity, growth may require old and new blocks simultaneously. These hooks do not replace mbedTLS allocation. [Policy/limits](../security_library_review.md). Public-key lookup authorizes a username/key pair, while wolfSSH verifies signed proof of possession. SSH host identity is a separate persisted P-256 key managed by `ssh_security`. A pending-result marker gates exactly-once signed-key completion/currentness; the reviewed wolfSSH version/feature profile is guarded and keyboard-interactive has an explicit rejecting callback (advertisement is not a dispatch filter). Consumed admin staging bytes and retired slots are wiped; this is not a full library-memory wipe guarantee.
Routing follows the authenticated role:
+9 -1
View File
@@ -17,6 +17,12 @@ This is a semantic map, not a complete file inventory. Start here, then read the
- Files: `src/security_build_policy.c`, registration in `src/CMakeLists.txt`, diagnostic flags in `sdkconfig.defaults`; tests: `tests/security_build_policy/run.py` (optional `--sdkconfig-header` checks the generated configuration).
- Compile-only guard: require no core dumps and silent panic reboot; reject panic/register output, panic/runtime GDB stubs and OCD-aware panic handling. No runtime allocation/task or physical JTAG restriction. Policy, operational profiles and target gates: [Phase 9 hardening](../security_hardening.md).
## Source-pinned dependency corrections (Phase 9C)
- Files: root `CMakeLists.txt` (after `project()`), `cmake/security_overrides.cmake`, `tools/security_overrides.py`; tests: `tests/sdk_security_overrides/run.py --build-dir .pio/build/esp32-s3-devkitc-1-n16r8`.
- Build input is the exact-hash original **plus checked-in edits**, not installed source alone. Generated copies replace four target sources without modifying SDK/managed components: HTTPS cleanup/private-key release, HTTPD scratch lifetime/null first read, ESP-TLS server-only protocol list, and wolfSSH password bounds/payload wiping. Original notices and compile properties retained; source/hash/target ambiguity fails configuration. Never hand-edit generated copies or silently repin.
- Policy/evidence/limits: [library review](../security_library_review.md), [Phase 9C](../security_hardening.md#9c-library-cleanup-and-protocol-policy). Source-contract tests must locate and verify actual generated compilation inputs, not assume original vendor paths.
## Secure randomness
**Responsibility:** provide the sole project-owned, mutex-serialized application DRBG, seeded before Wi-Fi/radio use.
@@ -71,6 +77,7 @@ This is a semantic map, not a complete file inventory. Start here, then read the
- Independent throughput diagnostics: `web_serial_transport.{c,h}` owns two fixed per-slot binary-TX aggregates and epoch fences; `web_console.c` exposes default-disabled `web performance enable|disable|show|clear`. Queue-entry/callback-entry, synchronous-send and completion/drain-return estimates, not peer receipt or scheduler-only latency. `tests/web_serial_performance/run.py`; resource/evidence limits and UART0 paired capture: `docs/web_throughput_diagnostics.md`.
- Storage compatibility: `user_database` persists missing storage empty and preserves valid v1 user bytes; private derived `v1_admin_marker`, no public bootstrap/migration/sync APIs. `web_security` privately migrates v1 1392-byte material to TLS-only v2 1340-byte material, exact identity/generation retained, commit before publish, fail closed without fallback overwrite. Credential commands removed; user generated passwords and TLS rotation remain. Contracts, downgrade and evidence limits: `docs/legacy_credential_removal.md`.
- Security files: `src/web_security.{h,c}`, `src/web_cookie_auth.{h,c}`, `src/web_session_store.{h,c}`, `src/web_auth_parse.{h,c}`. Private IDF boundary: `src/web_httpd_adapter.{h,c}`.
- Phase 9C web login: non-consuming early quota/epoch probe before body receive, authoritative reservation after parse; raw JSON wiped before KDF, credentials before error send. Existing verification-count/window/service-restart semantics retained. `tests/web_cookie_auth/run.py --admission` and domain regressions.
- HTTP policy/UI: `web_cookie_auth` + `web_auth_parse` enforce bounded cookie/Origin/CSRF/admin admission; `web_login_ui.{c,h}` serves login, `web_ui.c` owns session-fenced Serial/Admin/Settings and shared quick controllers. Tests: `tests/web_cookie_auth/run.py` (domain variants), `tests/web_auth_parse/run.py`, `tests/web_login_ui/run.py`, `tests/web_ui_session/run.py`.
- Admission diagnostics: `web_diagnostics.{c,h}`, `tests/web_diagnostics/run.py`; six post-TLS records/32-event opt-in ring, no HTTPD off-owner inspection. [Contract](../web_admission_diagnostics.md).
- Identity/lifecycle: `web_server_replace_identity()` + `web_security` reserve service before identity; commit before reserved stop/start, no rollback after commit. `web_lifecycle_settings.{c,h}` owns original-login ID/ACK handoff. Tests: `tests/web_security/run.py`, `tests/web_admin_transport/server_lifecycle.py`.
@@ -111,7 +118,8 @@ Shared UI regression: `tests/web_ui_session/run.py` and its domain `.cjs` fixtur
**Responsibility:** authenticate SSH, route users to serial and administrators to the command dispatcher, and own wolfSSH lifecycle.
- Files: `src/ssh_transport.{h,c}`, `src/ssh_auth_policy.{h,c}`, `src/ssh_security.{h,c}`, `src/ssh_console.{h,c}`
- Files: `src/ssh_transport.{h,c}`, `src/ssh_auth_policy.{h,c}`, `src/ssh_memory.{h,c}`, `src/ssh_protocol_policy.{h,c}`, `src/ssh_security.{h,c}`, `src/ssh_console.{h,c}`
- Phase 9C: global wolfSSL hooks securely retire unpoisoned IDF5.5 usable extents (no header; shrink retains capacity, grow can need both blocks). Five checked static-lifetime algorithm setters before context publication. Tests: `tests/ssh_memory/run.py` (optional `--idf-path` extent audit), `tests/ssh_protocol_policy/run.py` (generated feature/KEXINIT/context failure checks).
- Phase 9B admission: three owner-only boot-lifetime token buckets (handshakes, password/signed-key requests, unsigned probes); no restart/counter-clear reset. Explicit keyboard rejection, pending-result marker and version/feature guard preserve reviewed callback order. Consumed admin RX/accepted TX and retired slots are wiped. Tests: `tests/ssh_auth_policy/run.py`, `tests/ssh_auth_transport/run.py`, `tests/wolfssh_auth_contract/run.py` (requires installed vendor source and production compile database). [Policy/counters/limits](../security_hardening.md#9b-ssh-admission-and-credential-handling).
- Interfaces: init/start/stop, session snapshots/disconnect/revocation, host-key replacement, counters; `ssh_transport_get_management_snapshot()` / `ssh_transport_manage_current()` fence lifecycle and exact session admission. `ssh_transport_replace_identity()` reserves service before task-bound security identity across stop/commit/restart, retains context until all slots retire and rejects orphan starts. Tests: `tests/ssh_management/run.py`, `tests/ssh_management/security.py`, `tests/ssh_management/runtime.py`.
- Called by: startup, network clients, user revocation, console/local UI
+13 -1
View File
@@ -2,6 +2,18 @@
Working memory, not an implementation timeline. Source is authoritative; begin with [code map](code-map.md), then [architecture](architecture.md) and [decisions](design-decisions.md).
## Phase 9C — library cleanup / protocol policy — 2026-09-15
- User requested continuation; hardware still deferred to **whole Phase 9**, no per-slice approval gate. Initial Git status clean. Secure boot/encrypted NVS excluded; no eFuse/partition/dependency-version/asset changes, no SDK/managed source mutation.
- `tools/security_overrides.py` + `cmake/security_overrides.cmake` included after root `project()`: require exact IDF5.5/version/originalSHA/edit matches, generate four full notice-preserving source copies in build tree, replace exact component source preserving flags/includes. Compile inputs are pinned originals PLUS checked-in edits. Missing/changed/ambiguous target/source fails; reconfigure tracks originals/script/generated. Never edit derived files or blindly repin. Original sources remain unchanged; compiled dependency behavior intentionally changes.
- Overrides: HTTPS post-handshake allocation-failure TLS deletion, complete failed-start destruction and raw key pre-free wipe (failedstop owns live state); HTTPD scratch allocate/copy/wipe/free preserve old on failure, finalwipe, null first-read/nullable parserpointer fix; server-local TLS1.2 ECDHEECDSA AES128/256GCM, no renegotiation, no change clientdefaults/global primitives; wolfSSH GetSize both password lengths, failed newpassword framing skips callback, checked method suffix wiped before responses with prefix/canaries preserved, library asyncpending retains payload (project synchronous).
- `ssh_memory.{c,h}` installed as globalwolfSSL hooks before initialization, PSRAMpreferred/internalfallback, usableextent securefree/noheaders, shrink wipes tail retains capacity, growth old+new allocation failure preservesold. Guards unpoisonedIDF5.5; dynamicIDF TLS buffers compile-rejected for cleanup contract. These costs need actual peak/latency evidence; liveinline/compaction/stack/hardware intermediates not allwiped.
- `ssh_protocol_policy.{c,h}` applies checked staticlists before contextpublication: Curve25519/P256KEX, P256hostkey, AES128/256GCM, hmacsha256 advertisement, Ed25519/P256userkey advertisement (DB enforcesauth). LegacyCBC/CTR/removedKEX-only clients fail; no identitymigration. TLSpolicy is serveronly so future outboundHTTPS RSAclients unaffected.
- `web_cookie_auth`: nonconsuming earlyquota/epoch probe before receive, final postparse reservation unchanged; JSONwipe beforeKDF, credentialsafter/beforeerrors; handler-lifetime RetryAfter. Existing5verification/60s fixedwindow/restartreset/malformednotcharged retained; no challengefairness/generalrequestlimit claim. All10domain modes passed implementation.
- Final parent `pio run` PASS **94,340 B linked RAM / 1,831,309 B flash** (sameRAM/+1,384flash vs9B). Final fivefocused suites PASS incl installedSDK allocationextent, SDKcleanup/TLS/generator/nullfirstread+actual4source registration, SSHpolicy actual15contextintegration+KEXINIT, generatedwolfSSH135cases, web early admission. 17 related regressioncommands PASS before finalnullablefirstreadpatch; patchedSDKsuite+firmware rerunafter. Two independent reviews no blocking issues; inherited null-pointer subtraction found/fixed/tested. Standard UBSan linking unavailable earlier; new parser/allocator trap instrumentation passed in focused development. No realnetwork/hardware/cryptohandshake or reserveclaims.
- Bounded review in `docs/security_library_review.md`: normal inspected mbedTLS record/MPI/PK/HMAC and wolfECC scalar paths alreadywipe; newhooks cover observed retired DER/buffer gaps, not proof everycopy erased. Password1264ASCII/PBKDF2SHA25650k/generated24of64 unchanged pending costmeasurement; P256selfsigned20252049/trust verification retained; CSP/headers reviewed no blindHSTS or crypto-global removal.
- **Next: Phase9D current upstream advisory/license review and provisioning/rotation/reset/backup/recovery/decommissioning runbooks.** No external advisory/CVE review or full license audit performed by9C; do not describe pinned versions/localfixes as certified current. Any versionupgrade now must re-audit/source-rebase overrides. Target checklist in hardeningdoc adds modern/legacy suite negotiation, rekey, malformed encryptedpassword packets, TLS/scratch failurecleanup, securefree CPU and old+new allocationheadroom under fullmix. Do not wait for9Ctarget signoff tocontinue.
## Phase 9B — SSH admission / credential handling — 2026-09-15
- User requested continued Phase 9 work and will validate **the phase as a whole**. Do not pause between slices for target approval; all target gates remain unrun and collected in `docs/security_hardening.md`. Secure boot/encrypted NVS remain excluded; Phase 8 sign-off stays closed. Initial Git status for this slice was clean.
@@ -10,7 +22,7 @@ Working memory, not an implementation timeline. Source is authoritative; begin w
- Admin RX consumed spans / TX positively accepted spans are securely wiped; pending retry and serial hot-path bytes unchanged. Whole retired slot securely wiped before generation/fd restoration. `console_input` and shared remote hidden prompts reject overflow/unsupported bytes on submit, sticky across editing; visible CLI behavior preserved; existing callers prevent prefix persistence. Input errors wipe output and return zero length.
- Source audit verified actual pinned wolfSSH auth callback order and `SendChannelData` positive copied/consumed behavior. Tests pin `internal.c` SHA256 and execute extracted vendor parser/send functions with crypto/IO doubles plus actual compiler-feature preprocessing. Followup resolved reviewer concern about keyboard error-path one-byte write: inline buffer initialized, framed packets retain padding reserve, exact-sized protocol-identification pending state cannot reach auth, rejection purges without advancing length. This is a narrow invariant audit, not library security certification.
- Validation: parent `pio run` PASS **94,340 B linked RAM / 1,829,925 B flash** (+144RAM/+1,360flash vs9A; not runtime reserve). Parent and independent review PASS all four new suites `ssh_auth_policy`, `ssh_auth_transport`, `wolfssh_auth_contract` (35 vendor cases+resolved feature profile), `hidden_input`; token policy UB-sanitizer trap mode passed, standard UBSan runtime absent. Related 11 command suite PASS: SSH management/security/runtime, console boundary/accounts/lifecycle, admin SSH policy, web cookie SSH/accounts, web admin transport+tickets, security build policy18. `git diff --check` PASS. No blocking review findings; no upload/erase/device operations/deps/generated assets/commit.
- **Next:** complete remaining secret-lifetime and protocol review; inspect pinned library private-key/password-packet/buffer-growth/destruction before claiming comprehensive zeroization. Focused application audit found and fixed admin staging retention and hidden-input truncation; other checked key/candidate handoffs already wipe. TLS/SSH algorithm/certificate/header/password/KDF policy and web receive-before-throttle/challenge fairness remain review items. Dependency advisory/license review and lifecycle runbooks still planned, not performed. No need to await 9B hardware sign-off to continue.
- 9B's planned library/protocol and early web-admission follow-up is implemented and bounded by 9C above. Challenge fairness/full-memory wiping are not guaranteed. External maintenance/lifecycle work is next; no intermediate target sign-off needed.
- Final target checklist includes real SSH clients offering several keys, bad signatures/passwords, keyboard decline, each pool/refill/restart-clear persistence, established mixed transport/USB/UART0 responsiveness and reserve measurements, hidden-input errors/CRLF timing and crash recovery. UART0 paired/delayed LF relies on next-prompt flush; host UART fake does not model timing. No real-crypto/live packet-network or target claims from extracted-function tests.
## Phase 9A — crash/debug baseline — 2026-09-15
+12
View File
@@ -134,6 +134,18 @@ Only constraints supported by implementation or current project documentation be
**Consequence:** Re-audit SDK assumptions on upgrade; never patch around Origin `null` by weakening same-origin policy. Browser authentication POST uses CORS mode with fixed same-origin URLs/credentials because no-referrer non-CORS POST can serialize Origin as null. Digest-only cookie/challenge sessions replace Basic without fallback or live-record eviction. CSP loader hashes and authored scripts change atomically. Navigation preserves terminals/lease, while session-identity changes require a clean document before showing retained buffers. [Authentication and terminal contracts](../web_administration.md#authentication-and-admission).
## Dependency corrections are reproducible build inputs, not local SDK edits
**Decision:** Root CMake installs four exact-source-hash corrections after IDF target creation, rendering copies in the build tree and replacing each original target source exactly once. Original notices, includes and source compile properties are retained. Changed hashes/versions/missing/ambiguous sources fail configuration, with no unpatched fallback. Tests verify generated bytes and actual compiler inputs. [Registry and audit](../security_library_review.md).
**Consequence:** Upgrades need source/lifetime/feature re-audit, not just refreshed pins. Installed vendor code alone is not authoritative for overridden functions. Corrections cover HTTPS failure cleanup/key wiping, HTTPD scratch ownership, TLS server-local policy and SSH password packet bounds/wiping. TLS client defaults and global primitives remain unchanged. This is normal reproducibility checking, not tamper-resistant attestation.
## Retired library storage and protocol defaults have explicit policies
**Decision:** wolfSSL/wolfCrypt hooks use reviewed unpoisoned IDF5.5 usable allocation extents to wipe before free. No header overhead; shrink wipes tail but retains capacity; growth allocates/copies/wipes, retaining the old block on failure. Poisoned heaps and dynamic IDF TLS buffers are compile-rejected pending separate lifetime audits. Explicit static TLS/SSH allowlists replace negotiation defaults, without identity migration or weakening user-database authorization.
**Consequence:** Whole-buffer wipes and old-plus-new allocation peaks need combined target measurement; no blanket claim covers live inline residue, compiler spills or every crypto temporary. Legacy-only cipher/KEX clients can lose access; policy setter failure must free unpublished context rather than fall back. Web's early quota check does not change verification counts, service-restart resets or challenge fairness. [Exact algorithms, cleanup and limits](../security_library_review.md).
## Security material and configuration use bounded, versioned NVS records
**Decision:** Application settings, users, and identities use separate fixed/versioned NVS blobs. Serial, Wi-Fi, mDNS-hostname, and local-UI working edits are RAM-only until explicitly saved. User mutations and HTTPS/SSH identity changes commit directly as part of the operation. Invalid ordinary configuration generally selects RAM defaults without erasing storage; malformed security material fails closed and needs explicit reset.
+2
View File
@@ -157,6 +157,8 @@ HTTPS listens on port 443 only. Authenticate with any current user-database user
SSH listens on port 22 and accepts user-database passwords plus stored `ssh-ed25519` and `ecdsa-sha2-nistp256` public keys. wolfSSH verifies key possession after the database authorizes the username/key pair; unsigned key probes do not complete authentication. A `user` receives the broker-backed UART1 serial stream. An `admin` receives the administration shell instead, does not become a broker client, and cannot acquire a UART1 writer lease.
SSH algorithm policy is explicit: KEX `curve25519-sha256`/`ecdh-sha2-nistp256`, P-256 host key, AES-128/256-GCM ciphers and `hmac-sha2-256` MAC advertisement (GCM authenticates packets). CBC/CTR-only or excluded-KEX-only clients cannot connect. There is no CLI fallback that weakens this policy; do not rotate keys merely to address a negotiation mismatch. [Exact TLS/SSH policy and upgrade contract](security_library_review.md).
SSH admission uses global boot-lifetime token buckets: handshakes and password/signed-key requests each allow a burst of six and refill one token per ten seconds; unsigned-key probes allow twelve and refill one per five seconds. The existing three-counted-attempt failure closure remains per connection. Reconnect, `ssh stop`/`ssh start`, host-key rotation and `ssh clear-counters` do not replenish these budgets. Rate denial closes the authenticating connection, not an established stream. Restrict hostile traffic and allow natural refill; repeatedly reconnecting consumes shared capacity and can prevent other users from logging in.
`ssh counters` separates handshake/verification/probe admissions and rate rejections, attempt-limit closures, backend errors and rejected methods. Admitted work is not necessarily successful or completed; probes/rate denials are not completed `auth-attempts`. These counters expose no submitted credentials and clearing them does not change enforcement. Keyboard-interactive is explicitly rejected, not merely omitted from the advertised list. See [policy and validation](security_hardening.md#9b-ssh-admission-and-credential-handling).
+4 -4
View File
@@ -39,7 +39,7 @@ These constraints apply across all phases:
| 6 | Authenticated SSH serial transport | **Complete** |
| 7 | Local display and button interface | **Complete** |
| 8 | Role-based users and administrative access | **Complete (8D.22 accepted 2026-09-13)** |
| 9 | Security and production hardening | **In progress (9A/9B implemented; combined phase validation deferred)** |
| 9 | Security and production hardening | **In progress (9A9C implemented; combined phase validation deferred)** |
| 10 | Authenticated, rollback-capable OTA | **Planned** |
| 11 | BLE serial transport and provisioning evaluation | **Planned** |
| 12 | Advanced network integration | **Under evaluation** |
@@ -209,14 +209,14 @@ Phase 8 is complete for its accepted scope. Phase 9 has started at the user's re
### Phase 9 — Security and production hardening
**In progress.** Harden network authentication, secret lifetimes, crash/debug exposure and operational maintenance. Secure boot and encrypted NVS are explicitly excluded by user preference. No eFuse, partition, at-rest encryption or dependency-upgrade changes are part of 9A/9B; no future flash/PSRAM encryption commitment is implied. Physical extraction and firmware replacement remain outside the threat model after Phase 9, and software debug restrictions do not imply physical JTAG fuse restrictions.
**In progress.** Harden network authentication, secret lifetimes, crash/debug exposure and operational maintenance. Secure boot and encrypted NVS are explicitly excluded by user preference. No eFuse, partition, at-rest encryption or dependency-version upgrades are part of 9A9C; 9C adds source-pinned build-tree dependency corrections; no future flash/PSRAM encryption commitment is implied. Physical extraction and firmware replacement remain outside the threat model after Phase 9, and software debug restrictions do not imply physical JTAG fuse restrictions.
Staged work:
1. **9A — Crash/debug build policy and operational profiles — In progress; hardware pending.** `src/security_build_policy.c` requires `CONFIG_ESP_COREDUMP_ENABLE_TO_NONE=y` and `CONFIG_ESP_SYSTEM_PANIC_SILENT_REBOOT=y`; rejects core-dump enable/flash/UART, panic print/halt/GDBstub, runtime GDBstub and ESP/FreeRTOS debugger-aware options. `sdkconfig.defaults` makes the baseline explicit. Development/test/production use the same build baseline, not separate PlatformIO environments. Host matrix (`python3 tests/security_build_policy/run.py`) compiles the actual guard: 17 cases plus the generated-header check passed on 2026-09-15. `pio run` passed (94,196 B linked RAM / 1,828,565 B flash); target panic/recovery tests have not run. Production readiness remains pending.
2. **9B — SSH admission and credential handling — Implemented; combined target validation deferred.** Boot-lifetime, owner-only token buckets independently bound handshakes, password/signed-key requests and unsigned probes; reconnect/restart/counter clearing do not replenish them. Existing per-slot attempt limits/currentness remain. Explicit keyboard-interactive rejection, pending-signature result fencing, secret-free admission counters, consumed admin-buffer wipes and fail-closed hidden-prompt overflow/unsupported-byte handling are implemented. Four focused suites (including 35 pinned-vendor control-flow cases), 11 related regressions and `pio run` passed on 2026-09-15: 94,340 B linked RAM / 1,829,925 B flash. Global-budget starvation remains a documented tradeoff, not a solved availability problem.
3. **Next — Complete secret-lifetime and protocol review.** Audit remaining application/library allocations, private-key and packet-buffer destruction; review TLS/SSH algorithms, certificates, browser headers, password/KDF policy and remaining web-admission exposure. 9B's focused fixes are not full zeroization or protocol certification.
4. **Later — Maintenance and lifecycle.** Review dependency advisories and licenses without assuming pinned versions are permanently safe; document provisioning, rotation, factory reset, backup, recovery and decommissioning. OTA signing trust needs an independent policy without secure boot (Phase 10).
3. **9C — Library cleanup and protocol policy — Implemented; combined target validation deferred.** Exact-hash build-tree overrides correct HTTPS cleanup/leaks, HTTPD scratch failure/wiping/first-read handling, bounded SSH password parsing/wiping and server-local TLS policy without modifying installed dependencies. Secure wolfSSL allocation hooks and explicit SSH policy fail closed; early web quota probing avoids receiving already-throttled bodies. TLS1.2 ECDHE-ECDSA AES-GCM and SSH GCM/modern-KEX allowlists intentionally exclude legacy-only clients; no identity migration. Bounded password/certificate/header/destructor review is documented, not exhaustive zeroization. Final build PASS 94,340 B linked RAM / 1,831,309 B flash; focused and related host/source-contract tests passed. [Review and maintenance contract](security_library_review.md).
4. **Next — 9D maintenance and lifecycle.** Review current dependency advisories and licenses without assuming pinned versions or local corrections are permanently safe; document provisioning, rotation, factory reset, backup, recovery and decommissioning. Upgrades require re-auditing the source overrides. OTA signing trust needs an independent policy without secure boot (Phase 10).
At the user's request, hardware validation is deferred to **Phase 9 as a whole**, not required between implementation slices. [Security hardening](security_hardening.md) collects profiles, host evidence and the combined target checklist. Silent panic reboot removes useful crash diagnostics, not ordinary reset/boot/status information or every possible log disclosure. Raw flash/RAM/dumps remain secret-bearing, not routine diagnostic exports. Existing coredump bytes are not retroactively cleared; no secure erase is claimed. Isolated synthetic-secret debug builds require explicit reviewed source-policy changes, not a provided bypass flag.
+55 -7
View File
@@ -1,6 +1,6 @@
# Security hardening — Phase 9
**Status: in progress.** Phase 8 is complete at the accepted 8D.22 scope. **9A crash/debug policy and 9B SSH admission/credential handling** are implemented with host/build validation. At the user's request, hardware validation is deferred to **Phase 9 as a whole**, not an approval gate between implementation slices. This document records policy and procedures, not unrun passes or production certification.
**Status: in progress.** Phase 8 is complete at the accepted 8D.22 scope. **9A crash/debug policy, 9B SSH admission/credential handling and 9C library cleanup/protocol policy** are implemented with host/build validation. At the user's request, hardware validation is deferred to **Phase 9 as a whole**, not an approval gate between implementation slices. This document records policy and procedures, not unrun passes or production certification.
## Scope and threat model
@@ -65,6 +65,43 @@ The transport now wipes consumed admin RX bytes, positively accepted admin TX by
Hidden UART0 and shared remote-console prompts now reject overflow or unsupported bytes on submission with a wiped output buffer and `ESP_ERR_INVALID_SIZE`, rather than accepting a truncated/normalized prefix. The failure remains sticky after Backspace/Delete. Printable ASCII, CR/LF submission, Backspace/Delete and Ctrl-C retain their defined roles; visible command-line editing is unchanged. Existing callers prevent a rejected password or confirmation from reaching persistence. For pasted passwords, exceeding 64 characters or including unsupported bytes requires a fresh attempt; the password policy itself is unchanged.
## 9C library cleanup and protocol policy
[Library review and maintenance contract](security_library_review.md) records the scoped audit, corrected paths, existing cleanup and limits. This is not exhaustive library certification or a dependency security-release review.
### Reproducible source corrections
`tools/security_overrides.py` verifies full original-file SHA-256 values and ESP-IDF 5.5.0, applies exact-once edits, and generates four corrected sources under the build directory. `cmake/security_overrides.cmake`, included after `project()`, replaces exactly the corresponding sources in existing IDF/component targets, retaining compilation properties. Installed SDK/managed sources and their notices remain unchanged. Missing, changed or ambiguous sources fail configuration; there is no unpatched fallback. Do not edit derived files or repin a hash merely to make an upgrade build.
- **HTTPS:** delete TLS on post-handshake transport-allocation failure; fully destroy retained TLS configuration on failed HTTPD start; wipe the copied raw private key before free. Failed stop still retains live ownership.
- **HTTPD parser:** allocate/copy/wipe/free scratch on resize, preserve the old pointer on allocation failure, wipe final scratch, and handle the null initial parser pointer without undefined subtraction. Pending/unread bytes retain their existing behavior.
- **wolfSSH password parser:** bound both password lengths against the actual packet before application callbacks, reject malformed change-password fields without calling authentication, and wipe the bounded method-specific payload suffix before failure responses. Username/service/method prefixes remain intact. The current project callbacks are synchronous; library `WS_AUTH_PENDING` retains the payload for retry and is not claimed wiped.
- **ESP-TLS server configuration:** enforce the static-lifetime TLS list below before handshake setup; client defaults and global cryptographic primitives remain unchanged. IDF dynamic TLS buffers are rejected because their cleanup bypasses the reviewed upstream record-buffer wipe.
The new `src/ssh_memory.{c,h}` wolfSSL/wolfCrypt allocation hooks wipe the full owned usable allocation before release, including library import-failure and dynamic packet-buffer copies. They require the reviewed unpoisoned IDF 5.5.0 heap configuration; poisoning modes fail compilation rather than risking canary writes. No allocation header is added. Shrink retains capacity and wipes the tail; growth allocates/copies before wiping/freeing the old block, preserving it on allocation failure. PSRAM preference/internal fallback is unchanged. **Growth and HTTPD scratch resizing temporarily need both blocks; lower linked size is not evidence of safe runtime headroom.** Live inline buffers, in-place compaction tails, stack spills and every crypto intermediate are not comprehensively covered.
### Explicit network protocol policy
| Setting | Allowed values, in preference order |
|---|---|
| HTTPS version | TLS 1.2 only; server renegotiation disabled |
| 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 host key | `ecdsa-sha2-nistp256` |
| SSH ciphers, both directions | `aes128-gcm@openssh.com`, `aes256-gcm@openssh.com` |
| SSH MAC advertisement | `hmac-sha2-256` (GCM supplies packet authentication) |
| SSH user-key advertisement | `ssh-ed25519`, `ecdsa-sha2-nistp256`; database authorization remains authoritative |
`src/ssh_protocol_policy.c` uses permanent strings and checks every setter; any failure destroys the unpublished context without fallback. Tests verify the actual compiler's available algorithms and generated KEXINIT lists, not merely setter success. The server-only TLS correction avoids breaking future outbound HTTPS clients by globally removing RSA-certificate suites.
**Compatibility:** TLS CBC/CCM/ARIA-only and SSH CBC/CTR-only clients no longer connect; removed KEX-only clients also fail. Mainstream-client interoperability and rekey remain target gates, not host-test claims. Existing TLS/SSH identity and user-key storage need no rotation or migration. Password/KDF, certificate validity/trust and browser-header policy were reviewed and retained with documented limits; no blind KDF-cost increase or HSTS policy was introduced.
### Web admission and shorter plaintext lifetimes
A non-consuming quota/epoch check now runs after valid challenge consumption but before body receive. An already-exhausted verification budget returns 429/`Retry-After` without receiving/parsing credentials or calling the verifier; unread bodies still cause connection closure, not draining. The authoritative reservation remains after parsing. Raw JSON is wiped before verification, parsed credentials afterward, and both before error-response sending. Header strings remain live through synchronous serialization.
The existing global five-verifications/60-second fixed window is unchanged: malformed requests do not charge it, counter clear does not replenish it, and web service restart does. This differs deliberately from SSH's boot-lifetime buckets. The first boot-minute window remains anchored at uptime zero. Challenge monopolization, global-budget starvation and malformed-body work while budget is available are not solved by this early check.
## Operational profiles
These are handling and validation profiles of the **same supported build baseline**, not separate PlatformIO environments or selectable security overrides.
@@ -81,7 +118,7 @@ Raw flash, RAM and dumps can contain Wi-Fi passwords, private keys, password ver
## Validation gates
### Host and build — passed 2026-09-15 (9A and 9B)
### Host and build — passed 2026-09-15 (9A9C)
From the repository root:
@@ -93,15 +130,19 @@ python3 tests/hidden_input/run.py
pio run
python3 tests/security_build_policy/run.py --sdkconfig-header .pio/build/esp32-s3-devkitc-1-n16r8/config/sdkconfig.h
python3 tests/wolfssh_auth_contract/run.py
python3 tests/sdk_security_overrides/run.py --build-dir .pio/build/esp32-s3-devkitc-1-n16r8
python3 tests/ssh_memory/run.py
python3 tests/ssh_protocol_policy/run.py
python3 tests/web_cookie_auth/run.py --admission
```
Latest 9B `pio run` passed with **94,340 B linked RAM / 1,829,925 B flash**, +144 B RAM / +1,360 B flash against 9A. This is linked size, not measured runtime headroom. All four new focused host suites passed, including 35 pinned-vendor parser/control-flow cases and actual compiler-feature preprocessing. The crash-policy generated-header matrix passed all 18 cases. Related SSH management/security/runtime, console boundary/accounts/lifecycle/policy, cookie-auth SSH/accounts and browser-admin transport/tickets regressions passed (11 commands). Independent review and `git diff --check` passed. Use `CCACHE_DISABLE=1` on host commands if the compiler wrapper's cache is read-only in a sandbox. No upload, erase, eFuse operation or target test was performed.
Latest 9C `pio run` passed with **94,340 B linked RAM / 1,831,309 B flash**, unchanged linked RAM / +1,384 B flash against 9B. This is linked size, not measured runtime headroom. Five focused suites passed after final HTTPD first-read correction: pinned SDK cleanup/TLS/source registration, 135 generated wolfSSH parser/control-flow cases, secure allocator, SSH policy (including 15 actual context-integration cases), and web early admission/wiping. The allocator's optional installed-SDK extent contract was also run with the installed IDF path and passed; plain invocation reports that optional check skipped. Seventeen integrated regression commands passed before the final first-read addition, including SSH auth/management, HTTPD idle, HTTPS lifecycle, five cookie-auth modes and the 18-case crash-policy matrix. All ten existing cookie-auth domain modes also passed during implementation. Independent reviews found no blocking issues; the identified inherited first-read pointer issue was corrected and tested. Use `CCACHE_DISABLE=1` on host commands if the compiler wrapper's cache is read-only in a sandbox. No upload, erase, eFuse operation or target test was performed.
Record the revision, compiler/build outcome and effective configuration. Confirm that the matrix accepts the supported configuration, rejects each prohibited option independently, and rejects absent/disabled required settings. Confirm the normal firmware build compiles the guard. A rejected unsafe configuration is an expected negative-test result, not a firmware build pass. Neither these commands nor a successful build proves target panic behavior.
### Combined Phase 9 target validation — deferred, not run
Retain these checks for the final phase test session; do not stop implementation for a separate 9A/9B sign-off.
Retain these checks for the final phase test session; do not stop implementation for a separate 9A/9B/9C sign-off.
#### Crash and recovery
@@ -120,11 +161,18 @@ Retain these checks for the final phase test session; do not stop implementation
5. Test hidden credentials at maximum length and one byte over, different suffixes past the limit, unsupported input bytes, overflow followed by editing, Ctrl-C, disconnect and confirmation failure on UART0 and remote administration. No rejected prefix may be persisted or echoed. Check both CR/LF behavior, including delayed UART0 LF delivery: the current UART0 reader relies on next-prompt input flushing, unlike the remote reader's explicit paired-LF handling; host fakes do not prove device timing.
6. Exercise generated-password delivery with slow/partial remote output and short subsequent commands, then disconnect/reconnect. Application-buffer wipe assertions are host evidence; do not export live RAM to establish a device pass.
#### Protocol compatibility and allocation-failure recovery
1. Verify both allowed TLS suites and both SSH GCM ciphers using compatible clients; force excluded CBC/CTR/other-only offers and confirm rejection. Exercise both SSH KEX choices and both user-key types, initial handshake and rekey, plus TLS renegotiation rejection. Retain UART0 access; do not rotate identities to work around an algorithm mismatch.
2. With synthetic credentials, test truncated/oversized SSH password and change-password packets: no authentication callback for malformed fields, no crash, bounded disconnect/recovery. Host canary assertions are not real encrypted-packet coverage.
3. Exercise HTTPS failed-start, post-handshake allocation failure, normal/failed-stop retry and split-header scratch allocation failure on a separately reviewed fault-injection image. Observe recovery/no accumulating allocation loss without exporting keys or RAM. Failed stop must not prematurely free live TLS state.
4. Repeatedly start/stop HTTPS and SSH and stress header parsing/authentication under the full transport mix. Capture internal/DMA/PSRAM free/minimum/largest-block and stack margins alongside serial/broker loss counters. Specifically measure old-plus-new allocation peaks and secure-free CPU cost; previous very low internal minima remain important.
5. Verify exhausted web login returns early without stalled-body work, clears the used pre-login challenge, and recovers after the documented window. Check malformed requests below quota and correct credentials for normal behavior; do not infer fairness from a rate-limit pass.
## Staged next work
- **Next: complete the secret-lifetime and protocol review.** 9B fixes confirmed admin-buffer retention and hidden-input truncation, but does not complete the library-allocation audit. Inspect pinned private-key import/destruction, password-packet storage and buffer growth/free paths in mbedTLS/wolfSSL/wolfSSH before claiming full zeroization. Review negotiated TLS/SSH algorithms, certificate trust/validity, browser security headers and password/KDF policy without casually changing shared crypto or persisted identities.
- **Web admission review remains separate.** `src/web_cookie_auth.c` still limits credential verifications to five per 60-second fixed global window, shared across clients. This is not a sliding window or persistent account lockout; auth lifecycle restart resets it. Challenge starvation/global-budget starvation and the receive-before-throttle path remain review items, not changes delivered by 9B.
- **Later: maintenance and lifecycle.** Review ESP-IDF/wolfSSL/wolfSSH advisories and dependency licenses, then plan any upgrades separately. Complete provisioning, key rotation, backup, factory reset, recovery and decommissioning runbooks without claiming physical-extraction resistance or secure erasure.
- **Next — 9D maintenance and lifecycle.** Review current ESP-IDF/wolfSSL/wolfSSH advisories and third-party licenses, then plan any upgrades and source re-audits separately. Complete provisioning, key rotation, backup, factory reset, recovery and decommissioning runbooks without claiming physical-extraction resistance or secure erasure. No external advisory or complete license review has been performed by 9C.
- **Retained evidence limits:** 9C completes a bounded cleanup/protocol review, not every-library-copy zeroization. Live inline residue, compaction tails, hardware/stack intermediates, global admission starvation and resource/interop measurements remain documented limitations or combined target gates. Any additional hardening must preserve owner lifetimes and bounded recovery.
- **Phase 10: OTA trust.** Define independent image-signature verification, trust-anchor provisioning, rotation/revocation, rollback/downgrade and recovery policy without secure boot. Authenticated transport alone is not image-signing policy, and OTA signature checks cannot prevent physical firmware replacement.
See the [roadmap](roadmap.md#phase-9--security-and-production-hardening), [electrical procedures](electrical_tests.md) and [administration regressions](user_administration_tests.md) for wider gates. Production readiness remains pending; Phase 8 acceptance is not reopened by these follow-ups.
+143
View File
@@ -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: **1264 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.