Add SSH host identity rotation controls

This commit is contained in:
2026-09-13 19:58:05 +02:00
parent aa4bbc2c8c
commit 8df1d2218b
19 changed files with 626 additions and 107 deletions
+2
View File
@@ -152,6 +152,8 @@ The 8D.6 document binds retained terminal state to its first validated username/
### SSH
**8D.21 identity extension:** Existing SSH settings routes/controller/ID dispatcher slot expose atomic public stored fingerprint/generation and fixed supported ECDSA P-256 algorithm, plus rotation confirming service and identity generations. `ssh_transport_replace_identity()` takes existing command mutex with zero wait, compares service state then obtains task-bound nonreused security reservation before any stop/crypto/NVS. Canonical UART0/deferred SSH wrapper and direct security rotate/reset share admission; command mutex and identity reservation span stop→commit/publish→conditional restart. Crypto/NVS run outside security locks/spinlocks. Failed stop skips mutation/start; persistence failure can follow disconnection and attempts old-identity service recovery; committed replacement is never rolled back after restart failure. Stopped rotate stays stopped; canonical reset can recover unavailable material and start stopped SSH, without a browser Reset/recovery endpoint. Runtime owner retains context until all slots retire, frees it before clearing cleanup admission, and rejects orphan overwrites at start. wolfSSH copies caller DER; stack/candidate/live superseded key wiping retained. Public service/security snapshots are separate observations; admission compares both. Existing bounds256/768/96 bytes, login-isolated/manual15-second/no-replay result flow and shell policy unchanged; no HTTPS self-cutting ACK gate needed because HTTPS stays accessible. Changed-known_hosts verification requires trusted UART0 `ssh host-key info`. Full contract/evidence/parent-target limits: `docs/phase8d21_implementation.md`.
**Typed ordinary SSH controls (8D.19 first service slice):** `web_ssh_settings` owns one session-bound operation/result slot, with256-byte/four-receive JSON,768-byte safe two-row projection and96-byte result. Three optional current-admin routes use canonical cookie/Origin/CSRF protection and the existing four-entry dispatcher (IDs only), never lifecycle work on HTTPD. The dispatcher checks login/principal currentness and30-second dequeue deadline. `ssh_transport` copies only published state under its short lock, and conditional actions take the existing command mutex with zero wait before checking a saturated lifecycle generation; lifecycle comparison/admission shares that mutex with CLI. Disconnect publishes an exact SSH-ID close request under the existing SSH lock; only the owner closes sockets/wolfSSH. SSH session generations now retire exhausted slots, while a separate lifecycle generation fences stop/start ABA and survives counter clear. Failed/pending cleanup gates typed controls, retaining canonical UART0 recovery. UI confirms SSH/all-SSH/one-SSH scope, preserves stale selection without rebasing, and uses15-second bounded requests with manual result/refresh recovery, no automatic replay.36 handlers/six sockets and unchanged tasks/stacks/timers/queue depth. No invoking HTTPS-session-cutting action, web-session/USB/Wi-Fi controls or identity mutation. Full contracts, admitted-work/timeout limits and pending target checks: `docs/phase8d19_implementation.md`.
`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.
+2
View File
@@ -124,6 +124,8 @@ This is a semantic map, not a complete file inventory. Start here, then read the
## SSH
- **8D.21 SSH identity extension:** `ssh_security.{c,h}` owns zero-wait atomic public metadata and task-bound/nonreused identity reservations shared by direct rotate/reset, crypto/NVS outside security locks. `ssh_transport_replace_identity()` shares canonical CLI/deferred SSH combined service-before-identity admission, existing command-mutex reservation and stop→commit→conditional restart; failed stop skips mutation/start, context retained until owner retires every slot, start rejects orphan handles. `web_ssh_settings.c`/existing `web_ui.c` SSH controller/routes add P-256 fingerprint/algorithm/both generations and confirmed rotation (same256/768/96-byte bounds, manual15-second/no-replay result flow); no browser Reset/recovery/export or shell-policy change. PASS real-security+combined-owner5, runtime-retention, management5, cookie SSH7+shared/all variants, UI158 and broad HTTPS/console/transport regressions. Final100,556 RAM/1,828,573 flash (+24/+3,500), CPU160 unchanged. Full21 allowed HTTPS+SSH implementation complete; independent SSH parent review and target acceptance pending. Contracts/tests/lifetime/failure/limits: `docs/phase8d21_implementation.md`.
- **8D.19 first service slice:** `web_ssh_settings.{c,h}` adds optional admin-only GET `/api/settings/ssh`, GET/POST `/api/settings/ssh-operation`; existing dispatcher queues only IDs to one login-bound slot. `ssh_transport_get_management_snapshot()` copies published state without owner wait/stack scan; `ssh_transport_manage_current()` checks saturated service generation under existing command mutex and exact session ID under SSH lock before canonical lifecycle/external-close admission. Exhausted session slots retire instead of wrapping. `web_ui.c` adds confirmed SSH-only Settings, sticky stale selection,15-second requests/manual Check Result/Refresh.36 handlers/six sockets/no new tasks/timers/depth/stacks/assets; CPU160 and8D.18 preserved. Tests `tests/ssh_management/run.py`, cookie `--ssh`, dispatcher, lifecycle27 and UI143. Contracts/resources/remaining8D.19 service audit/target checks: `docs/phase8d19_implementation.md`. SSH slice implemented/host/build verified; parent review/target sign-off pending, not full8D.19.
**Responsibility:** authenticate SSH, route users to serial and administrators to the command dispatcher, and own wolfSSH lifecycle.
+2
View File
@@ -4,6 +4,8 @@ This file is working memory. Update it during active work and before handoff; do
## Development state
- **8D.21 remaining SSH identity slice implemented end-to-end (2026-09-13), host/build verified, independently reviewed; target acceptance pending:** Clean initial worktree; completed HTTPS history/shared reservations preserved. `ssh_security` adds atomic zero-wait public fingerprint/generation and task-bound nonreused reservation shared by direct rotate/reset; crypto/NVS outside security locks, commit-before-publication/wipe. `ssh_transport_replace_identity()` holds existing command mutex across service-before-identity comparison/reservation and canonical stop→persist→conditional restart, fails stop without mutation/new start, preserves stopped rotate/reset behavior and partial effects. Runtime start refuses orphan handles; failed stop retains context until owner retires all slots. Existing cleanup already freed wolfSSH sessions before broker failure—no preexisting UAF demonstrated. Existing19 routes/256-byte request/768-byte snapshot/96-byte result/ID dispatcher slot and UI now expose P-256 metadata and confirmed rotation with both generations, all-SSH/known_hosts/trusted UART0 `ssh host-key info`/partial-effect warnings,15-second/manual result/no replay; HTTPS stays accessible. No browser Reset/recovery/export, user authorized-key, deprecated shell-policy, task/timer/queue/route/assets/config/SDK/dependency changes. PASS SSH security5 including real-mbedTLS/NVS+actual combined-owner integration, runtime retention, management5, cookie SSH7+all prior variants/shared, UI158+HTML/CSP, HTTPS lifecycle44+2/security17, dispatcher/policy and broad regressions. Final production pio21.98s **100,556 RAM/1,828,573 flash (+24/+3,500 vs SSH baseline100,532/1,825,073)**; CPU160 defaults/active/generated verified, combined WS send untouched. Full21 allowed HTTPS+SSH identity implementation scope complete, not target/M3/8D.22 acceptance. Independent review found no confirmed actionable defects; installed wolfSSH copy/lifetime, owner failures and auth/UI scope audited. Reviewer reran SSH security5/retention/management/cookie SSH7/UI158/console suites/HTTPS lifecycle+security17/diff PASS. Parent final pio confirmation PASS7.15s100,556/1,828,573 B; diff check PASS. No target sign-off inferred. Full contracts/tests/resources/exclusions and hardware/runtime limits: `docs/phase8d21_implementation.md`. No upload/erase/commit/branch.
- **8D.21 HTTPS-first slice implemented end-to-end (2026-09-13), host/build verified, independently reviewed; target sign-off pending:** Preserved initial three-file audit documentation work, then completed shared `web_server_replace_identity()` service-before-identity reservation across generation/commit/stop/start, zero-wait public `web_security` fingerprint/generation projection and nonreused reservation shared by direct canonical rotate/reset. Crypto/NVS outside locks; no identity mutation before stale service rejection, no CLI/browser-shell bypass, no rollback after commit. CLI reset/recovery starts a stopped service; stopped ordinary rotation stays stopped. Existing8D.20 lifecycle routes/slot/ACK/original-login dispatcher and shared UI controller extend with confirmed `rotate`, required identity generation and320-byte seven-field snapshot. Fingerprint/both generations confirmed; trusted UART0 `web certificate info`, changed trust/fresh login, all-web-session disruption, partial-effect uncertainty,15-second UI bounds/manual results/no replay retained. No browser reset/export/recovery secrets, SSH identity work, new tasks/timers/routes/queues/assets/config/SDK/dependencies. PASS security17, lifecycle44+two integrated production-owner/real-mbedTLS/NVS fault groups, cookie lifecycle8+shared/all variants, UI156+renderer/HTML/CSP, dispatcher/console self-detach and broad regressions. Final pio22.78s **100,532 RAM/1,825,073 flash (+24/+3,568 vs audited100,508/1,821,505)**; defaults/active/generated CPU160 verified, combined WS send and prior20/18/19/throughput work preserved. Independent reviewer found no confirmed actionable findings, reran security17/lifecycle44+2/cookie lifecycle/UI/dispatcher/console lifecycle/diff PASS. Parent final pio confirmation PASS6.98s100,532/1,825,073 B; diff check PASS. Final expanded integration rerun PASS; no hardware/upload/erase/commit/branch or runtime-reserve/M3 claim. Exact ownership/wrap/failure/API/tests/resources and pending parent/target checklist: `docs/phase8d21_implementation.md`. **Chosen HTTPS slice complete; full21 incomplete, remaining SSH identity work separately requested.**
- **Historical 8D.21 audit-only handoff (2026-09-13), superseded by implemented HTTPS slice above:** Read exact plan and relevant memory/source; enumerated HTTPS-first public metadata + confirmed rotation, excluded redundant healthy-material reset UI, all SSH identity work and recovery secrets/database recovery. Verified TLS-only reset additionally recovers unavailable material and starts a stopped HTTPS service at the CLI; preserve those semantics. Existing security mutex protects generation/commit but not restart; CLI and browser-shell callers commit then stop/start separately. Completion requires combined service/identity generation admission and reservation shared with canonical callers,8D.20 ACK path, complete typed API/UI and fault/concurrency/auth/no-replay tests—not unused prerequisite interfaces. Baseline `pio run` PASS7.17s100,508 RAM/1,821,505 flash; initial worktree clean. No source/test/device/config changes or regression/final-build/review/sign-off claim. Only audit handoff documentation delivered; chosen HTTPS slice and full21 both incomplete. Exact findings, exclusions and continuation checklist: `docs/phase8d21_implementation.md`. Do not mistake this entry for feature delivery; durable architecture/code-map/decisions remain unchanged because no owner contract changed.
+2
View File
@@ -104,6 +104,8 @@ Phase 8D.2 adds a third identity: non-reused 64-bit originating web-session IDs
## Selected self-affecting admin SSH actions use bounded deferred control
**8D.21 SSH host identity decision:** Reuse canonical deferred SSH self-rotation and typed19 SSH routes/dispatcher, not command strings or another executor. Compare/reserve service then identity before stop or storage; retain the existing command mutex and task-bound nonreused security token through canonical stop→commit→conditional restart. Direct security rotate/reset share identity exclusion. Crypto/NVS run outside security locks/spinlocks; failed stop must not mutate identity or attempt start, and failed persistence may already have disconnected SSH. Never roll back committed identity after restart failure. Retain wolfSSH context until every slot retires; only owner frees it before clearing cleanup admission, and start refuses orphan handles. Public metadata is zero-wait/atomic within security, not a cross-owner authorization; UI confirms both generations and warns changed known_hosts/trusted UART0 `ssh host-key info`, partial effects and no replay. HTTPS remains accessible, so existing SSH queue/response order is retained without20's self-cutting ACK gate. Canonical reset/recovery preserved but excluded from browser as duplicate healthy rotation plus distinct recovery semantics. Details and test limits: `docs/phase8d21_implementation.md`.
**8D.20 integrated typed HTTPS/reboot decision (supersedes prerequisite-only status below):** A successful synchronous HTTP response send return is the ACK boundary, not peer receipt. Queue exactly one nonreused-ID HTTPD callback after sending; that callback only submits an ID to the existing dispatcher, never waits or runs lifecycle. No captured request/fd/reusable slot pointer. A lost callback remains reserved even after its two-second admission deadline; only its arrival or successful HTTPD destruction releases that reservation. Do not retry queue submission or permit callback accumulation. Original-login/current-admin/post-validation30-second deadline precede canonical generation-conditional owner admission; no cancellation claim after admission, including detach failures and deliberately login-invalidating reserved restart. Typed reboot uses shared `esp_restart()` outside locks after HTTPS generation reservation, not a console string/self-cleanup wait or new runner. UI requires explicit confirmation, fences15-second whole requests and late results, retains unknown/duplicate gates across navigation, never restores/retries mutations and requires fresh login after HTTPS restart. Network controls remain the sole Wi-Fi domain; USB is UART1 serial recovery, not administration or uninterrupted whole-device reboot. Exact contracts and target limits: `docs/phase8d20_implementation.md`.
**Historical 8D.20 HTTPS owner prerequisite (superseded above):** Conditional HTTPS stop/restart compares an expected saturated lifecycle generation under the canonical server mutex, not snapshot-check-unlock-unconditional-stop. Restart retains transition ownership through stop and start; a failed stop never admits start, and failed cleanup requires canonical recovery. Repeated init must not clear the retained lifecycle failure; counter clear must not reset generation. The zero-wait management projection does not authorize a request or prove reachability. All lifecycle execution remains off HTTPD and outside the server mutex during owner waits. Future typed ACK handling must precede admission on the existing dispatcher, with original-login currentness; later revocation is not cancellation of an admitted restart. No ACK/API/UI/reboot integration exists in this prerequisite, and the full phase remains incomplete. `src/web_server.{c,h}`, `docs/phase8d20_implementation.md`.