# Phase 8D.11 — SSH authorized keys ## Status (2026-09-08) Implemented on user request; host-tested and firmware-build verified. Target validation/sign-off remains pending. Accepted 8D.8–8D.10 and M2 remain accepted; this is not M3 completion or authorization for 8D.12. ## User-supplied target telemetry (2026-09-08) User supplies a fresh-boot sample at 60 seconds and a later full-client-mix sample. The last browser serial/admin connection again required several attempts. This records partial target evidence, not functional key-management acceptance or phase sign-off; exact flashed revision, browser, key algorithms, traffic duration and cleanup/soak results were not supplied. | Memory (bytes) | Boot free / minimum / largest | Loaded free / minimum / largest | |---|---|---| | Internal 8-bit | 69,584 / 58,076 / 31,744 | 32,596 / 5,468 / 22,528 | | Internal DMA | 61,828 / 50,320 / 31,744 | 24,840 / 532 / 22,528 | | PSRAM | 8,246,356 / 8,242,108 / 8,126,464 | 8,087,636 / 8,056,956 / 7,995,392 | Minima are conservative sums of per-region lifetime minima, not a simultaneous sample; internal/DMA capabilities overlap and must not be added. Low minima are a transient-pressure warning, not proof of allocation failure. Settled loaded free internal/DMA is comparable to the prior accepted 8D.10 sample (32,556 / 24,800), but lifetime minima are substantially lower (previously 19,228 / 11,472). Different connection attempts/workloads prevent attributing that difference to 8D.11. Boot: SSH/HTTPS started once without reported startup failures; all supplied network request/session counters zero; no broker clients, UART stopped, USB attached but host closed. mDNS announced with ESP_OK. Five accounts/two admins, no cookie sessions. SSH owner stack 20,480 bytes, minimum-free 18,468. Loaded: two successful SSH public-key sessions (admin console and user serial writer); two browser serial observers, USB observer and active browser admin. Broker writer 16, web observers 17/19, USB observer 10, all pending/events zero at the sample. UART running at 230400 8N1 RTS/CTS, DTR active, RX/TX queues empty; USB host open. Two cookie sessions; mDNS/SSH/HTTPS report operational, no transition/error. SSH minimum-free stack 16,276 bytes; HTTPD/dispatcher stack margins unreported. Public-key authentication works for these two sessions, but algorithms and whether keys were imported through the new UI are unspecified. Admission evidence: HTTPS handshake attempts logged from 197688 through 379338 ms, with three errors at 377948/378298/378658 ms: `mbedtls_ssl_handshake returned -0x0050`, TLS session creation and HTTPD accept failure. Installed mbedTLS `net_sockets.h` defines this as `MBEDTLS_ERR_NET_CONN_RESET`, not allocation failure. It does not establish why the connection reset or whether transient heap pressure caused delays. The previously accepted intermittent last-browser admission issue has recurred and remains unresolved. Loaded counters: SSH TCP connections/handshake success/auth attempts 2/2/2; handshake/auth/timeouts/capacity/IO failures zero. SSH broker connect 1, writer request/grant 1/1, revocations 1; admin admission 1/no failures, stream RX/accepted/rejected 7/7/0, TX 5,977, session revocations zero. Browser admin tickets issued/consumed 2/2, connected/disconnected 2/1, TX 272; rejection/capacity/protocol/backpressure/send/queue counters zero. Web requests 89 authenticated/zero auth failures, root 2/status 76/tickets 8/assets 2, response errors zero; tickets issued/consumed/expired 8/5/3. Serial WS connect/disconnect 5/3 with zero admission/service/broker failures; RX zero; TX 218 binary frames/21,200 bytes and 16 control frames/1,322 bytes; writer requests/grants/denials 5/1/4. One send failure/close, zero queue/protocol failures. Cookie login attempts 2, invalid/throttled/capacity/CSRF/logout zero. No broker drop-counter output supplied; zero pending queues is not proof of lossless traffic. Application counters do not account for every pre-HTTP TLS failure. Documentation-only evidence update: no new build, source/config edits, upload, erase, device commands or sign-off. Next diagnostic evidence should correlate connection attempts with heap/allocation failures and browser network timing/socket occupancy rather than assume heap exhaustion or increase socket capacity blindly. Key-workflow checklist and reserve approval remain open. ## Scope and contracts - Accounts settings lists selected-account key types, stable slot indices and SHA256 fingerprints; imports OpenSSH Ed25519/P-256 public-key text and explicitly deletes one slot or clears all keys. - `user_database.{c,h}` provides a zero-wait fingerprint-only snapshot and conditional key mutation wrappers. Username/account ID/auth generation are checked under the canonical database mutation lock. Existing CLI mutation, persistence, duplicate/capacity, curve validation and SSH authentication semantics are retained. - `web_account_settings.{c,h}` admits strict JSON and uses the existing account-operation slot and administration dispatcher. Successful mutations best-effort revoke only the target's web/SSH sessions; currentness checks remain authoritative. Self changes can invalidate the response/result login: 401/disconnect proves neither success nor cancellation. Inspect after relogin before retrying. - Optional **POST `/api/settings/accounts/keys`** accepts `{username,user_id,auth_generation}` and returns `{username,user_id,auth_generation,keys:[{index,type,fingerprint}]}`. This read uses POST to retain the existing bounded JSON/Origin/CSRF policy rather than introduce query parsing. Stale/absent selection returns 409; unavailable snapshot returns 503. No public-key blobs, verifier material or private keys are returned. - Existing POST `/api/settings/account-operation` adds `key-add` with `public_key`, `key-delete` with `key_index` (0–2), and `key-clear`. All carry target identity. Request bodies remain **768 bytes/four receives**, public-key text at most **384 decoded bytes**, decoded SSH blob at most **128 bytes**, three keys per account. Envelope/base64/schema rejection occurs at admission; canonical blob/curve validation runs on the dispatcher. Results remain bounded, secret-free, replaceable and session-bound; no mutation auto-retry. - UI confirms mutations, clears pasted text on submission/context changes, uses text-only rendering and fences stale navigation/session/target work. Completion uses existing bounded polling followed by account/key refresh. Stable key slots may be sparse: option values and deletion confirmation resolve by actual index, not array position. - `web_server.c` registers the listing route independently through the allocation-safe optional POST helper. **24 handlers**, six sockets/no LRU; no new tasks, stack-size increases, queue-depth changes or broker lease changes. Larger operation/local structures still have runtime stack costs requiring target measurement. - No private-key upload/export, SSH host-identity management, browser-shell restriction changes, UART0 recovery changes or generated asset regeneration. ## Validation Parent integrated runs passed: - `python3 tests/admin_console_boundary/accounts.py`: canonical key validation/authorization, Ed25519/P256, malformed/off-curve/truncated keys, zero-wait snapshots, stale ID/generation/recreation, duplicate/capacity, sparse slots, failed persistence and CLI parity. Crypto host adapters use OpenSSL; this is not target SSH validation. - `python3 tests/web_cookie_auth/run.py --accounts`: shared authentication policy and account/key route admission/execution tests. - `python3 tests/web_ui_session/run.py`: initial integrated 64 groups and subsequent sparse-slot production fix, plus renderer/HTML/CSP checks. Final test-only extension independently passed **68 browser groups** (agent-run), including sparse `[1]`/`[0,2]`, deletion/automatic refresh, and invalid/duplicate indices. - `python3 tests/web_admin_transport/server_lifecycle.py`: **16 groups**, including isolated optional-route failure and restart recovery (dependencies faked). - `pio run`: final production build **PASS, 14.85 s**, **96,076 B RAM / 1,703,685 B flash**; **+168 B RAM / +9,448 B flash** versus recorded final 8D.10. The later four UI test groups change no firmware source. - `git diff --check` passed for the integrated production change; the final test extension also passed its scoped check. Backend agent additionally reports passing console boundary `run.py` and cookie `--serial-settings`. Independent source review identified the sparse-slot UI assumption; corrected production selection/validation and added dedicated regressions. No other concrete review findings were reported. Host tests are not real network/concurrency/stack-margin proof. ## Pending target checklist 1. Import real Ed25519 and P-256 public keys, compare fingerprints with CLI/ssh-keygen, authenticate with corresponding private keys, and verify existing password authentication remains available. 2. Exercise maximum input, malformed/private-key text rejection, duplicates and full three-key capacity. Delete slot 0 while later slots survive; list/delete/clear sparse slots and re-add successfully. 3. Mutate another account through browser and CLI; verify target revocation, unrelated sessions retained, persisted keys after reboot, stale selection rejection and failed-operation recovery. 4. Exercise own-account key changes: immediate revocation and uncertain lost acknowledgement, relogin/inspect without automatic replay. Confirm last-key removal does not imply password removal. 5. Run full supported serial/admin mix, preserving one writer and isolated observers; record heap/internal-DMA largest blocks, HTTPD/dispatcher stack margins and cleanup/repeated-operation behavior. 6. Obtain user target sign-off separately. No upload, erase, hardware test, sanitizer run or commit was performed by this task.