Harden SSH Admission And Credential Input
This commit is contained in:
@@ -126,9 +126,9 @@ Cookie login/logout replaces Basic/cache. Digest-only records carry copied princ
|
||||
|
||||
Typed SSH settings use the existing ID dispatcher and original-login result slot, never HTTPD wolfSSH calls or owner waits. Conditional lifecycle/session controls compare a saturated service generation and exact nonreused session ID under canonical locks. `ssh_transport_replace_identity` reserves service then identity before stop, retaining the command mutex across stop → commit → conditional restart. Failed stop skips mutation/start; failed persistence may follow disconnection; committed identity is never rolled back after restart failure. Only the SSH owner frees context after all slots retire, and start rejects orphan handles. Direct security/CLI/deferred SSH callers share task-bound identity reservations; crypto/NVS run outside security locks. HTTPS remains available, so no self-cutting HTTP ACK gate is needed. [SSH contracts](../web_administration.md#ssh).
|
||||
|
||||
`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.
|
||||
`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`.
|
||||
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.
|
||||
|
||||
Routing follows the authenticated role:
|
||||
|
||||
|
||||
@@ -111,7 +111,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_security.{h,c}`, `src/ssh_console.{h,c}`
|
||||
- Files: `src/ssh_transport.{h,c}`, `src/ssh_auth_policy.{h,c}`, `src/ssh_security.{h,c}`, `src/ssh_console.{h,c}`
|
||||
- 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
|
||||
- Dependencies: user database, broker, admin SSH console, secure random, wolfSSH/wolfSSL; boot start gate requires Wi-Fi and SSH security/runtime readiness, independently of HTTPS identity readiness (verified in `main.c` after accepted legacy cleanup).
|
||||
@@ -142,6 +143,7 @@ Shared UI regression: `tests/web_ui_session/run.py` and its domain `.cjs` fixtur
|
||||
- Flow: `UART0/admin SSH/browser admin -> bounded request queue -> one dispatcher -> esp_console_run()`
|
||||
- Ownership: dispatcher is sole `esp_console_run()` caller; the SSH owner exclusively performs post-initialization wolfSSH runtime calls.
|
||||
- Lifecycle: remote session tokens include slot generation; fixed output/history/prompt state is wiped immediately on idle close or after an executing handler returns. Admin SSH `exit` and Ctrl+D on an empty command line request bounded deferred self-disconnect after best-effort output draining.
|
||||
- Hidden input: UART0 and shared remote prompts reject overflow/unsupported bytes on submit with wiped output, sticky across edits; visible editing is unchanged. Tests: `tests/hidden_input/run.py` plus console boundary regressions.
|
||||
- Constraint: one slow command or prompt serializes all administration. Admin SSH is unavailable until command registration and UART frontend creation complete; supported deferred actions wait only for a bounded application-buffer drain heuristic.
|
||||
|
||||
## Wi-Fi
|
||||
|
||||
@@ -2,6 +2,17 @@
|
||||
|
||||
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 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.
|
||||
- `ssh_auth_policy.{c,h}`: 72-byte owner-only boot-lifetime state, independent handshake and password/signed-key buckets (capacity6, refill1/10s), unsigned-probe bucket(capacity12, refill1/5s). No waits, allocations, per-peer maps or NVS writes. Reconnect, stop/start/rotation/counter clear do not replenish; no refunds, idle saturation/no excess credit, clock regression fails closed. Global starvation is a deliberate documented tradeoff; natural refill only after hostile traffic subsides, not fairness/zeroCPU protection.
|
||||
- `ssh_transport` gates handshake before wolfSSH allocation and credentials before database/ordinary signature work; keeps existing per-slot three-counted-attempt closure and 15s deadline. Explicit pending-result marker fences duplicate/unexpected completion. wolfSSH1.4.20 and certificates/none-disabled guard; keyboard prompt rejection callback/context prevents unregistered callback dispatch while keeping password/publickey advertisement. New aggregate admission/probe/throttle/limit/backend/method counters via `ssh counters`; `add_counter` saturates (do not generalize to unrelated direct lifecycle increments).
|
||||
- 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.
|
||||
- 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
|
||||
|
||||
- User requested Phase 9 and explicitly excluded secure boot and encrypted NVS. Roadmap now marks Phase 9 in progress; Phase 8 acceptance remains closed. Physical extraction/firmware replacement stay outside the threat model even after hardening; no encryption/eFuse/partition changes.
|
||||
@@ -9,7 +20,7 @@ Working memory, not an implementation timeline. Source is authoritative; begin w
|
||||
- `docs/security_hardening.md` defines shared operational profiles, secret-bearing artifact handling, evidence limits and target gates. Reserved partitions unchanged; old dump/credential copies are not erased. No generated assets/dependency changes.
|
||||
- Validation: `pio run` PASS **94,196 B linked RAM / 1,828,565 B flash**. Host policy matrix PASS17; actual generated SDK header PASS as eighteenth case. Initial host test hit read-only ccache storage; `CCACHE_DISABLE=1` rerun passed. Independent review found no actionable issues and repeated both host modes and diff check. No upload, erase, hardware validation or commit.
|
||||
- **9A target gate remains open:** synthetic-secret controlled panic, no register/UART/flash dump, reboot rather than halt, UART0/USB/network recovery and broker behavior. No test-only panic command was added to production.
|
||||
- **Next implementation: SSH cross-connection authentication throttling.** Read-only audit verified three counted attempts per slot reset on reconnect, with unsigned key probes uncounted; web already gates five verifications per fixed global 60-second window. Choose bounded monotonic admission and explicit counters without sleeping the SSH owner, preserve currentness/slot ownership and test real wolfSSH callback ordering. No auth behavior changed in 9A. Remaining zeroization/crypto/dependency/license/lifecycle audits are planned, not completed.
|
||||
- 9A changed no authentication behavior; its planned SSH follow-up is now implemented in 9B above. Whole-phase target validation remains deferred.
|
||||
|
||||
## Web popup cosmetics — 2026-09-14
|
||||
|
||||
|
||||
@@ -104,6 +104,18 @@ Only constraints supported by implementation or current project documentation be
|
||||
|
||||
**Consequence:** Shared remote-console slots require transport-qualified tokens and immutable owner adapters. Validate owner currentness outside console locks, then recheck identity. Owner-side HTTPD/SSH IO and generation-safe cleanup remain mandatory; session liveness checks do not cancel executing handlers. Browser-shell permissions are parsed and narrower than typed Settings. [Authentication](../web_administration.md#authentication-and-admission), [console policy](../web_administration.md#browser-shell-policy).
|
||||
|
||||
## SSH admission budgets survive service and session lifetimes
|
||||
|
||||
**Decision:** Three fixed-size owner-only token buckets independently admit handshakes, password/signed-key requests and unsigned probes. Budgets last for the boot, not the slot/service/counter epoch. Rejection closes without sleeping; no per-account/IP storage or persistent lockout is added. Signed-key completion has an explicit pending-result marker and retains authoritative principal checks. Pinned library version/feature/source-contract tests protect callback order; keyboard-interactive is explicitly rejected.
|
||||
|
||||
**Consequence:** This bounds admitted work but permits global-budget starvation; restarting SSH is not an immediate recovery override. Established streams bypass admission, while actual load/latency still needs whole-phase device evidence. Counters are observations, never enforcement state. [Policy and tests](../security_hardening.md#9b-ssh-admission-and-credential-handling).
|
||||
|
||||
## Hidden input must not accept a truncated credential
|
||||
|
||||
**Decision:** Hidden console prompts reject overflow/unsupported bytes at submit, wiping output; rejection stays sticky after edits. Submission, Backspace/Delete and Ctrl-C retain their roles. Visible CLI editing is unchanged. Consumed SSH admin RX and accepted TX spans are wiped without touching pending retry bytes; slot retirement securely wipes before restoring generation/sentinels.
|
||||
|
||||
**Consequence:** Overlong/unsupported pastes must be retried; caller errors prevent prefix persistence. Application wipes do not establish library/stack/PSRAM zeroization. The pinned wolfSSH positive-send contract is copied/consumed bytes, not peer receipt. UART0 paired-CRLF timing remains a target check. Tests: `tests/hidden_input/run.py`, `tests/ssh_auth_transport/run.py`, `tests/wolfssh_auth_contract/run.py`.
|
||||
|
||||
## Typed serial mutations share the administration dispatcher
|
||||
|
||||
**Decision:** Typed domains queue IDs to the existing serialized dispatcher, never CLI strings or secrets. One original-login slot per domain and a nonreused ID fence stale work; session/deadline checks precede canonical admission. Results are replaceable observations, not durable history/idempotency.
|
||||
|
||||
@@ -32,7 +32,7 @@ Browser `web` allows only status/stop/exact forced certificate rotation; `wifi`/
|
||||
| `user key clear <username> --force` | Delete all public keys for an account. |
|
||||
| `user recover --force` | When normal user-database initialization failed, explicitly replace only its blob with an empty database; UART0-only, refuses a healthy database. |
|
||||
|
||||
Usernames must match `[a-z][a-z0-9_-]{0,15}`. Passwords contain 12–64 printable ASCII characters. The fixed database supports eight users and three SSH keys per user; initial key types are `ssh-ed25519` and `ecdsa-sha2-nistp256`. A key may be assigned to multiple accounts but cannot be duplicated within one account. Password verifiers, salts, raw key blobs, and passwords are absent from ordinary status output. `Ctrl-C` cancels a password or key prompt, and generated passwords are shown once.
|
||||
Usernames must match `[a-z][a-z0-9_-]{0,15}`. Passwords contain 12–64 printable ASCII characters. Hidden console prompts reject overflow or unsupported bytes rather than accepting a truncated/normalized prefix, even if later editing reduces the length; submit or cancel and start again. CR/LF submits, Backspace/Delete edits, and Ctrl-C cancels. The fixed database supports eight users and three SSH keys per user; initial key types are `ssh-ed25519` and `ecdsa-sha2-nistp256`. A key may be assigned to multiple accounts but cannot be duplicated within one account. Password verifiers, salts, raw key blobs, and passwords are absent from ordinary status output. `Ctrl-C` cancels a password or key prompt, and generated passwords are shown once.
|
||||
|
||||
Missing `user_db/database` storage is committed empty. On UART0 run `user add <username> admin`, optionally with `--generate`, to create the first administrator. There is no bootstrap command, imported shared credential, or synchronization with HTTPS material. Existing valid v1 user databases load unchanged, including previously migrated role-`user` accounts; no account is silently promoted.
|
||||
|
||||
@@ -157,6 +157,10 @@ 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 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).
|
||||
|
||||
UART0 and admin SSH submit to one bounded queue, and one dispatcher task is the sole caller of `esp_console_run()`. Consequently, SSH commands execute the canonical UART0 handlers and produce the same status and mutation behavior rather than using a second command implementation. Remote output is routed into the authenticated session's bounded output ring; only the SSH transport task accesses wolfSSH.
|
||||
|
||||
UART0 and admin SSH use shared whole-line Tab completion. A unique/common prefix expands inline; a Tab that cannot extend an ambiguous prefix prints the matching candidates and redraws the unchanged input line instead of cycling candidates. Admin SSH additionally supports four-entry per-session command history with Up/Down, inline cursor editing with Left/Right, Home/End (including Pos1/Ende terminal sequences), Backspace/Delete, Ctrl-C, and visible or no-echo interactive prompts. Its history is RAM-only, private to the session, and wiped on disconnect. Ping callbacks enqueue bounded typed results so all formatting remains on the dispatcher task.
|
||||
|
||||
+5
-5
@@ -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 hardware validation pending)** |
|
||||
| 9 | Security and production hardening | **In progress (9A/9B 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,16 +209,16 @@ 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 this first slice; 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-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.
|
||||
|
||||
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. **Next — SSH cross-connection authentication throttle.** Current SSH has three counted authentication attempts per session, reset on reconnect; HTTPS already admits five credential verifications per 60-second fixed global window. Define bounded reconnect-resistant admission/backoff, recovery and secret-free counters; test isolation from established sessions, UART0 and USB.
|
||||
3. **Later — Secret-lifetime and protocol review.** Audit zeroization across application/mbedTLS/wolfSSL/wolfSSH allocations; review TLS/SSH algorithms, certificates, browser headers and password policy.
|
||||
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).
|
||||
|
||||
[Security hardening](security_hardening.md) defines profiles and concrete host/build/hardware gates. 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.
|
||||
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.
|
||||
|
||||
### Phase 10 — Authenticated OTA and rollback
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Security hardening — Phase 9
|
||||
|
||||
**Status: in progress.** Phase 8 is complete at the accepted 8D.22 scope. Phase 9 starts with **9A crash/debug build policy**; host matrix and firmware build validation passed on 2026-09-15, and target-hardware validation has not run. 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 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.
|
||||
|
||||
## Scope and threat model
|
||||
|
||||
@@ -23,6 +23,48 @@ The host matrix in `tests/security_build_policy/run.py` compiles the actual guar
|
||||
|
||||
Silent panic reboot deliberately sacrifices panic text, register dumps and backtraces for reduced crash disclosure. Reset-reason/boot information and ordinary status/logging can remain; neither silence across the full boot sequence nor general log redaction is guaranteed. A monitor exception decoder cannot reconstruct a backtrace that was never emitted.
|
||||
|
||||
## 9B SSH admission and credential handling
|
||||
|
||||
### Boot-lifetime admission budgets
|
||||
|
||||
`src/ssh_auth_policy.{c,h}` owns three independent, fixed-size token buckets. Only the SSH owner task accesses the shared 72-byte policy; no allocation, per-peer/account map, timer task or sleep is added.
|
||||
|
||||
| Admission class | Initial/maximum burst | Refill |
|
||||
|---|---:|---|
|
||||
| New SSH handshake | 6 | One token per 10 seconds |
|
||||
| Password or signed-key authentication request | 6 | One token per 10 seconds |
|
||||
| Unsigned public-key probe | 12 | One token per 5 seconds |
|
||||
|
||||
These are **burst-plus-refill limits**, not six/twelve requests in every rolling minute. All peers/accounts and both slots share each class. Idle refill stops at capacity; denials do not extend the refill deadline. Reconnects, service stop/start, identity rotation and `ssh clear-counters` do not replenish the pools. Reboot starts a new policy lifetime. Timestamp regression fails closed. No persistent account lockout or NVS write is introduced.
|
||||
|
||||
- A handshake token is taken after finding capacity but before `wolfSSH_new()`/handshake work. Full-capacity rejection takes no token; later allocation/IO failure does not refund it.
|
||||
- Password/signed-key admission precedes database verification/authorization and ordinary key signature work. Success, invalid credentials, backend errors and rejected password-change requests do not refund admission. Unsigned probes use their own pool and cannot authenticate.
|
||||
- Exhaustion shuts down/rejects the new or authenticating connection without waiting inside the owner task. Already-authenticated streams do not pass through this admission gate. The existing three-counted-attempt failure closure, two-slot bound and 15-second handshake deadline remain.
|
||||
- **Availability tradeoff:** a client can consume the handshake burst by opening/abandoning connections and race legitimate clients for each refill. Global verification/probe pools can also starve other users. This bounds admitted work, not fair access or immunity to denial of service. TCP accept/rejection work and library parsing still occur; target latency under abuse is not yet measured. Restrict network access, stop the offending traffic and allow natural refill rather than repeatedly reconnecting/restarting. UART0/USB remain independent of these pools; HTTPS keeps its separate policy.
|
||||
|
||||
### Callback and library contract
|
||||
|
||||
`src/ssh_transport.c` requires wolfSSH 1.4.20, certificates disabled and `none` authentication disabled at compile time. The reviewed parser calls ordinary-key authorization before signature verification; rejected authorizations and unsigned probes have no result callback. Password results are completed within the password callback. An explicit pending-result marker fences signed-key completion; duplicate/unexpected/closing-session results cannot promote a principal or count another completed attempt. Principal currentness is still checked at successful signature completion and route admission.
|
||||
|
||||
Advertising only password/publickey is not a dispatch filter in this wolfSSH version. An explicit rejecting keyboard-interactive prompt callback and per-slot context prevent its unregistered-callback path; it creates/sends no prompts and closes the connection. The advertised methods remain password/publickey. This does not certify every malformed-packet path in the library.
|
||||
|
||||
`tests/wolfssh_auth_contract/run.py` checks the reviewed `internal.c` SHA-256 and version, preprocesses the actual build's feature profile, and executes extracted vendor parser/send functions with narrow crypto/IO doubles. A same-version source change requires re-audit, not blindly replacing the hash. It does not replace real-client/cryptographic integration testing. The positive `SendChannelData()` return contract means the caller's accepted prefix has been copied, including its consumed-data WANT_WRITE case; it is not peer acknowledgement.
|
||||
|
||||
### Counters and secret lifetime
|
||||
|
||||
`ssh counters` adds aggregate-only diagnostics:
|
||||
|
||||
- `handshakes` / `handshake-throttled`: admitted handshake work / rate-denied connections, separate from capacity failures.
|
||||
- `verifications` / `verification-throttled`: admitted password/signed-key requests / rate-denied requests. Admission does not imply the verifier ran or completed.
|
||||
- `probes` / `probe-throttled`: admitted/denied unsigned-key lookups, not completed credential attempts.
|
||||
- `attempt-limit-closes`, `backend-errors`, `method-rejects`: three-attempt closures, database auth/authorization/currentness errors, and rejected callback-level methods (including keyboard). These are not counts of every malformed SSH packet.
|
||||
|
||||
Existing `auth-attempts`/`auth-failures` remain completed counted outcomes; rejected password changes count, unsigned probes and rate-denied requests do not. Signed-key results finalize once after authorized work. These admission/auth counters saturate at `UINT64_MAX`, contain no submitted credentials/identities, and may be cleared independently of enforcement state.
|
||||
|
||||
The transport now wipes consumed admin RX bytes, positively accepted admin TX bytes, and the full retired slot while retaining its generation. Partial/retry paths preserve pending bytes. Serial-route hot-path behavior is unchanged. This shortens application plaintext lifetime; it is not a claim that wolfSSH/wolfSSL/mbedTLS, stack or PSRAM copies are all erased.
|
||||
|
||||
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.
|
||||
|
||||
## Operational profiles
|
||||
|
||||
These are handling and validation profiles of the **same supported build baseline**, not separate PlatformIO environments or selectable security overrides.
|
||||
@@ -39,32 +81,49 @@ Raw flash, RAM and dumps can contain Wi-Fi passwords, private keys, password ver
|
||||
|
||||
## Validation gates
|
||||
|
||||
### Host and build — passed 2026-09-15
|
||||
### Host and build — passed 2026-09-15 (9A and 9B)
|
||||
|
||||
From the repository root:
|
||||
|
||||
```sh
|
||||
python3 tests/security_build_policy/run.py
|
||||
python3 tests/ssh_auth_policy/run.py
|
||||
python3 tests/ssh_auth_transport/run.py
|
||||
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
|
||||
```
|
||||
|
||||
Validation: 17 synthetic/default compile cases passed; the generated SDK-header check passed as the eighteenth case. `pio run` passed with 94,196 B linked RAM and 1,828,565 B flash. The initial host invocation was blocked by the compiler wrapper's read-only cache location; rerunning with `CCACHE_DISABLE=1` passed. Independent policy review and `git diff --check` passed. No upload, erase, eFuse operation or target test was performed.
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
### Target hardware — not run; required for 9A acceptance
|
||||
### 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.
|
||||
|
||||
#### Crash and recovery
|
||||
|
||||
1. On an isolated synthetic-secret target, record the tested image/configuration and capture UART0 at 115200 baud. Verify normal boot, UART0 administration, native USB UART1 access, HTTPS and SSH before fault testing.
|
||||
2. Through separately reviewed test-only fault injection, trigger a controlled panic with the supported build policy intact. Verify reboot rather than halt/debugger wait, no panic register/backtrace output and no UART core dump. Record any remaining boot/reset information; do not promise complete UART silence.
|
||||
3. Verify no new flash core dump is written using a reviewed target-side pass/fail check that does not export partition contents. Distinguish old partition contents from a new write; do not erase the partition merely to claim this test passed.
|
||||
4. After reboot, verify UART0 recovery and USB serial access, then authenticated HTTPS/SSH and broker writer/observer behavior. With network services unavailable, verify UART0 and native USB still work. Review routine status/log output using synthetic secrets; this is bounded evidence, not universal redaction proof.
|
||||
5. Record outcomes and limitations before marking 9A complete. Device flashing/fault injection requires a separately authorized hardware session; no eFuse changes, partition migration or erase is required by this policy.
|
||||
5. Record outcomes and limitations in the combined Phase 9 acceptance. Device flashing/fault injection requires a separately authorized hardware session; no eFuse changes, partition migration or erase is required by this policy.
|
||||
|
||||
#### Authentication, input and loaded isolation
|
||||
|
||||
1. On a restricted test network using synthetic credentials, exercise password and Ed25519/P-256 key login for both roles, including a client offering multiple keys. Verify unsigned probes, wrong passwords/signatures, stale-principal rejection and normal shell admission. Explicit keyboard-interactive requests must close/reject without a crash or prompt.
|
||||
2. Exhaust each admission class separately, respecting the independent budgets. For verification testing reuse admitted connections (up to the existing three-failure limit) so handshake exhaustion does not mask the verification gate. Verify counter deltas, reconnect resistance, natural refill and that successful logins also consume capacity. Unsigned probes must not increase completed `auth-attempts`.
|
||||
3. From UART0, clear counters and stop/start SSH while exhausted; observe that neither grants fresh tokens. Account for time elapsed during these operations. Do not assume that a reconnect failure indicates bad credentials. A quiet 60-second period replenishes all pools; ongoing hostile traffic can keep them depleted.
|
||||
4. Keep an established SSH serial stream and USB/browser clients active while generating bounded invalid-login/reconnect traffic. Record serial/broker drops, UART0 command latency, SSH stream responsiveness, internal/DMA minima and recovery. Do not use this admission policy to claim zero CPU impact; TCP/kernel work, KDF/signature work within budget and two-slot occupancy still matter.
|
||||
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.
|
||||
|
||||
## Staged next work
|
||||
|
||||
- **Next: bounded SSH cross-connection throttling.** `src/ssh_transport.c` currently closes after three counted failed authentication attempts in a session; slot reset/reconnect resets the budget. Not every protocol message is counted (for example, an unsigned public-key probe is not a failed signed authentication). `src/web_cookie_auth.c` already limits credential verifications to five per 60-second fixed global window, shared across clients; this is not a sliding window or persistent account lockout, and auth lifecycle restart resets its state. Define reconnect-resistant SSH admission/backoff, bounded state and secret-free counters without blocking established sessions, UART0 or USB.
|
||||
- **Later: zeroization and protocol policy.** Audit secret lifetimes and failure cleanup across application, mbedTLS, wolfSSL and wolfSSH; review crypto algorithms, certificate trust, browser security headers and password policy. Existing wipes are not proof that every library/stack/PSRAM copy is cleared.
|
||||
- **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.
|
||||
- **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.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user