From 436c27adb138cf94e691c37ab71002869b73cfe4 Mon Sep 17 00:00:00 2001 From: Commander1024 Date: Tue, 15 Sep 2026 20:11:40 +0200 Subject: [PATCH] Enforce crash-safe build policy Add compile-time checks for silent reboot, disabled core dumps, and disabled debugger-aware panic handling. Include regression coverage, hardening guidance, and update Phase 8/9 project status. --- README.md | 13 ++-- docs/agent/code-map.md | 5 ++ docs/agent/current-state.md | 11 +++- docs/agent/design-decisions.md | 8 ++- docs/roadmap.md | 37 +++++------ docs/security_hardening.md | 71 +++++++++++++++++++++ sdkconfig.defaults | 16 ++++- src/CMakeLists.txt | 1 + src/security_build_policy.c | 30 +++++++++ tests/security_build_policy/README.md | 17 ++++++ tests/security_build_policy/run.py | 88 +++++++++++++++++++++++++++ 11 files changed, 267 insertions(+), 30 deletions(-) create mode 100644 docs/security_hardening.md create mode 100644 src/security_build_policy.c create mode 100644 tests/security_build_policy/README.md create mode 100644 tests/security_build_policy/run.py diff --git a/README.md b/README.md index 8faccd0..c20dd5e 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ ESP32-S3 firmware for a secure, multi-transport RS-232 adapter. It operates one ## Development status -Hardware characterization, serial/USB/Wi-Fi/HTTPS/SSH and local display/control are implemented and hardware-validated. **Phase 8 role-based users and administration is complete:** 8A–8C were target-hardware validated and the user explicitly signed off tested firmware at **8D.22 (2026-09-13)**. See the [roadmap](docs/roadmap.md#phase-8--role-based-users-and-administrative-access--complete) and [acceptance evidence](docs/web_administration_acceptance.md). Very low internal/DMA lifetime minima remain a nonblocking headroom follow-up, not an approved reserve. Phase 9 hardening begins only on a separate request. +Hardware characterization, serial/USB/Wi-Fi/HTTPS/SSH and local display/control are implemented and hardware-validated. **Phase 8 role-based users and administration is complete:** 8A–8C were target-hardware validated and the user explicitly signed off tested firmware at **8D.22 (2026-09-13)**. See the [roadmap](docs/roadmap.md#phase-8--role-based-users-and-administrative-access--complete) and [acceptance evidence](docs/web_administration_acceptance.md). Very low internal/DMA lifetime minima remain a nonblocking headroom follow-up, not an approved reserve. **Phase 9 security hardening is in progress**, starting with 9A crash/debug build policy; host/build validation passed and target-hardware validation is pending. Production readiness is not yet established. See [security hardening](docs/security_hardening.md) for scope, operational profiles, and validation gates. ### Browser administration @@ -32,6 +32,7 @@ Keep UART0 ready for administrative recovery and native USB for network-independ - [Electrical tests](docs/electrical_tests.md): OLED/buttons, MAX3243, UART loopback, and session-broker verification procedures. - [Role-based user database and UART0 administration](docs/user_administration_tests.md): user provisioning and administration, HTTPS/SSH authentication, session revocation, and reusable integrated web-administration regression procedures (not execution evidence). - [Web administration](docs/web_administration.md): current bounded API/owner and recovery contracts; [acceptance evidence](docs/web_administration_acceptance.md) records sign-off and telemetry limits. +- [Security hardening](docs/security_hardening.md): Phase 9 scope, crash/debug baseline, diagnostic handling, and pending validation. - [Command reference](docs/command_reference.md): UART0/admin-SSH administration, serial, broker, USB, Wi-Fi, mDNS, web, SSH, and diagnostic commands. ## Flash partition layout @@ -43,15 +44,15 @@ The N16R8 target has 16 MiB flash and 8 MiB octal PSRAM. PlatformIO uses the cus | `nvs` | `0x009000` | 512 KiB | Serial, Wi-Fi, mDNS hostname, local-display, role-based user, HTTPS identity, and SSH host-key data | | `otadata` | `0x089000` | 8 KiB | Active OTA-slot selection metadata | | `phy_init` | `0x08B000` | 4 KiB | Optional PHY initialization data | -| `nvs_key` | `0x08C000` | 4 KiB | Reserved for future encrypted-NVS keys | -| `coredump` | `0x08D000` | 128 KiB | Reserved for flash core dumps | +| `nvs_key` | `0x08C000` | 4 KiB | Unused; retained for layout compatibility (NVS encryption excluded) | +| `coredump` | `0x08D000` | 128 KiB | Unused by the supported crash policy; retained for layout compatibility | | `ota_0` | `0x0B0000` | 4 MiB | Primary application/OTA slot | | `ota_1` | `0x4B0000` | 4 MiB | Alternate application/OTA slot | | `storage` | `0x8B0000` | 7488 KiB | Reserved for future LittleFS web assets, logs, and files | Application offsets are aligned to the ESP32-S3's required 64 KiB boundary. The final storage partition ends at `0x1000000`, exactly the end of the 16 MiB flash chip. -The table reserves OTA and storage space; it does not implement OTA downloads, rollback confirmation, core-dump handling, NVS encryption, or filesystem mounting. +The table reserves OTA and storage space; it does not implement OTA downloads, rollback confirmation, NVS encryption, or filesystem mounting. Phase 9A disables new core dumps; it does not clear any existing `coredump` contents. The unused `nvs_key` and `coredump` reservations remain unchanged for layout compatibility; neither implies future enablement or secure erasure. ### One-time migration from the default partition table @@ -88,7 +89,9 @@ Serial, Wi-Fi, and mDNS hostname edits remain in RAM until explicitly saved with ## Security notes -The HTTPS interface uses a device-specific self-signed certificate and a same-origin login page with bounded server-side cookie sessions; HTTP Basic is no longer accepted. Open `/` or `/login`, sign in with a user-database password, and use **Sign out** before switching accounts. Four sessions have a one-hour absolute lifetime, including active serial connections; logout closes only that session's serial access. Login is globally limited to five credential verifications per 60 seconds, with explicit capacity/backoff errors. Direct-IP and mDNS access use separate host-only Secure/HttpOnly/SameSite=Strict cookies. Non-browser clients also require cookies, strict Origin and CSRF for mutations rather than Basic credentials. There is no plaintext HTTP or TCP serial listener. SSH accepts role-based passwords and authorized Ed25519/ECDSA P-256 public keys. User passwords are stored as salted PBKDF2-HMAC-SHA256 verifiers, but the HTTPS private key, SSH private key, and Wi-Fi credentials remain recoverable from unencrypted application-owned NVS blobs. Offline password guessing and stale append-oriented flash copies also remain possible. The reserved `nvs_key` partition does not enable encryption. Do not treat this firmware as resistant to physical flash or RAM extraction until the planned hardening work is complete. +The HTTPS interface uses a device-specific self-signed certificate and a same-origin login page with bounded server-side cookie sessions; HTTP Basic is no longer accepted. Open `/` or `/login`, sign in with a user-database password, and use **Sign out** before switching accounts. Four sessions have a one-hour absolute lifetime, including active serial connections; logout closes only that session's serial access. Login is globally limited to five credential verifications per 60 seconds, with explicit capacity/backoff errors. Direct-IP and mDNS access use separate host-only Secure/HttpOnly/SameSite=Strict cookies. Non-browser clients also require cookies, strict Origin and CSRF for mutations rather than Basic credentials. There is no plaintext HTTP or TCP serial listener. SSH accepts role-based passwords and authorized Ed25519/ECDSA P-256 public keys. User passwords are stored as salted PBKDF2-HMAC-SHA256 verifiers, but the HTTPS private key, SSH private key, and Wi-Fi credentials remain recoverable from unencrypted application-owned NVS blobs. Offline password guessing and stale append-oriented flash copies also remain possible. The reserved `nvs_key` partition does not enable encryption. Physical flash/RAM extraction and firmware replacement remain outside the threat model even after Phase 9. Secure boot and encrypted NVS are explicitly excluded; no flash/PSRAM encryption or physical JTAG eFuse restriction is promised. + +The Phase 9A supported build baseline requires disabled core dumps and silent panic reboot, rejecting panic print/halt/GDBstub and software debugger-aware options at compile time. Development, test, and production are operational profiles of the same build baseline, not separate PlatformIO environments. Silent panic reboot sacrifices panic backtraces/register dumps; reset-reason/boot information and ordinary status/logging can remain. This is not a general log-redaction guarantee. Treat raw flash, RAM and dumps as secret-bearing; do not export them as routine diagnostics. No retroactive dump clearing or secure-erase claim is made. See [security hardening](docs/security_hardening.md) for the pending checks and reviewed synthetic-secret debugging procedure. ## License diff --git a/docs/agent/code-map.md b/docs/agent/code-map.md index b7fcd17..4d9ec2e 100644 --- a/docs/agent/code-map.md +++ b/docs/agent/code-map.md @@ -12,6 +12,11 @@ This is a semantic map, not a complete file inventory. Start here, then read the - Dependencies: every subsystem initializer - Lifecycle constraint: optional display/network failures should not remove UART0 administrative recovery or USB UART1 access; the custom administration frontend starts only after command registration. +## Crash/debug build policy + +- 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). + ## Secure randomness **Responsibility:** provide the sole project-owned, mutex-serialized application DRBG, seeded before Wi-Fi/radio use. diff --git a/docs/agent/current-state.md b/docs/agent/current-state.md index 5e71fdb..459ddb6 100644 --- a/docs/agent/current-state.md +++ b/docs/agent/current-state.md @@ -2,6 +2,15 @@ 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 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. +- `src/security_build_policy.c` enforces resolved no-core-dump + silent-panic-reboot settings and rejects panic/register output, runtime/panic GDB stubs and OCD-aware handling. Registered unconditionally in CMake; explicit defaults. No runtime task/buffer added. This intentionally removes decoded panic traces, not ordinary boot/status logging or physical debug access. +- `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. + ## Web popup cosmetics — 2026-09-14 - `src/web_ui.c`: contextual buttons now occupy entire status cards; bounded grid columns and clipped/ellipsized values prevent narrow-window overflow. Quick panels preserve the visible terminal title, admin toolbar and selected-view indication; full-page promotion retains drafts and restores Settings presentation. @@ -28,7 +37,7 @@ Working memory, not an implementation timeline. Source is authoritative; begin w - Extremely low internal/DMA lifetime minima warrant correlated transient-headroom investigation; overlapping capability pools and conservative/non-simultaneous region minima do not prove allocation failure. Numeric reserves, HTTPD/dispatcher stack margins, peak correlation and detailed soak/fault evidence remain unapproved/unreported. Do not reopen functional sign-off or invent a reserve threshold. - SDK TLS `-0x004C` is generic NET_RECV_FAILED, not OOM. Two boot auth failures plausibly involve stale cookies, but causation is unconfirmed. Prior intermittent web admission issues and accepted idle cleanup do not justify claiming every admission failure fixed. - Real DNS/reannouncement, NVS power-loss, browser geometry/accessibility, individual fault cases and exact duration claims require explicit evidence if investigated; retained [regression procedures](../user_administration_tests.md) are not execution records. -- **Next: Phase 9 security/production hardening only if requested.** No source, configuration, test implementation, build, upload, erase, branch or commit is authorized by documentation consolidation. +- The earlier documentation-only handoff is superseded by the user's Phase 9 request; current scope and evidence are recorded above. Device operations remain unperformed. ## Scope and safety to retain diff --git a/docs/agent/design-decisions.md b/docs/agent/design-decisions.md index d80ee43..8c5372d 100644 --- a/docs/agent/design-decisions.md +++ b/docs/agent/design-decisions.md @@ -134,7 +134,7 @@ Only constraints supported by implementation or current project documentation be ## NVS is persistence, not a physical security boundary -**Decision:** The current firmware stores Wi-Fi credentials and TLS/SSH private keys in unencrypted application NVS. The reserved NVS-key partition does not enable encryption. +**Decision:** The current firmware stores Wi-Fi credentials and TLS/SSH private keys in unencrypted application NVS. The reserved NVS-key partition does not enable encryption. Phase 9 explicitly excludes secure boot and encrypted NVS by user preference; physical extraction and firmware replacement remain outside the supported threat model, not deferred guarantees of this phase. **Rationale/evidence:** `partitions.csv`, README security notes, and current code show no NVS-encryption setup. Original rationale for deferring encryption is outside the implementation; the observable limitation is explicit. @@ -142,6 +142,12 @@ Only constraints supported by implementation or current project documentation be **Relevant files:** `partitions.csv`, `README.md`, `src/web_security.c`, `src/ssh_security.c`, `src/wifi_config.c` +## Supported builds do not export crash memory + +**Decision:** Phase 9A requires disabled core dumps and silent panic reboot, and forbids panic/runtime GDB stubs and OCD-aware panic handling. `src/security_build_policy.c` checks resolved SDK configuration at compile time; defaults alone are insufficient. Development, test and production handling profiles share this baseline, with no bypass flag. + +**Consequence:** Decoded panic traces/register output are deliberately lost; ordinary boot/status diagnostics remain and are not universally sanitized by this policy. No eFuse, physical JTAG restriction, partition change or retroactive dump clearing is implied. Target panic/recovery validation remains necessary. [Policy and tests](../security_hardening.md). + ## Wi-Fi callbacks enqueue; the manager owns policy **Decision:** ESP event callbacks copy bounded event data into the Wi-Fi manager queue. A permanent manager task performs driver operations, profile/AP policy, deadlines, reconciliation, and station mDNS announcement transitions. mDNS initializes at most once, remains allocated across transient disconnects while its component handlers withdraw/re-enable the STA interface, and treats failure as nonfatal. diff --git a/docs/roadmap.md b/docs/roadmap.md index ca0c1c5..48c8f19 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -38,8 +38,8 @@ These constraints apply across all phases: | 5B | Offline xterm.js WebSocket serial terminal | **Complete** | | 6 | Authenticated SSH serial transport | **Complete** | | 7 | Local display and button interface | **Complete** | -| 8 | Role-based users and administrative access | **In progress (8A–8C complete; integrated web administration 8D planned)** | -| 9 | Security and production hardening | **Planned** | +| 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)** | | 10 | Authenticated, rollback-capable OTA | **Planned** | | 11 | BLE serial transport and provisioning evaluation | **Planned** | | 12 | Advanced network integration | **Under evaluation** | @@ -205,27 +205,20 @@ Completed implementation (8A–8C target-hardware validated; 8D explicitly accep ## Current and planned phases -Phase 8 is complete for its accepted scope. Phase 9 is next **only on a separate user request**; later work remains planned or under evaluation. Optional features must not weaken completed serial and recovery paths. General release gates below remain guidance for future work, not claims that every listed fault, soak or reserve measurement was individually performed for 8D. +Phase 8 is complete for its accepted scope. Phase 9 has started at the user's request; later work remains planned or under evaluation. Optional features must not weaken completed serial and recovery paths. General release gates below remain guidance for future work, not claims that every listed fault, soak or reserve measurement was individually performed for 8D. ### Phase 9 — Security and production hardening -Harden stored credentials, boot integrity, update trust, failure diagnostics, and dependency maintenance. This phase must be staged carefully because some ESP32 eFuse operations are irreversible. +**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. -Planned work: +Staged work: -- Define development, test, and production security profiles. -- Enable and validate NVS encryption for Wi-Fi, HTTPS, and SSH material. -- Evaluate flash encryption and PSRAM encryption for the target hardware and threat model. -- Enable secure boot with a documented signing-key and recovery process. -- Define secret-safe core-dump handling; do not expose credentials or session material in diagnostics. -- Audit key and session-material zeroization across application, mbedTLS, wolfSSL, and wolfSSH allocations. -- Add authentication throttling, lockout/backoff, and security-event counters where appropriate. -- Review TLS/SSH algorithms, certificate policy, password policy, and browser security headers. -- Review current wolfSSL/wolfSSH and ESP-IDF security releases instead of treating integration versions as permanent pins. -- Recheck all third-party licenses and preserve source/license notices. -- Document provisioning, key rotation, factory reset, backup, recovery, and device decommissioning. +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. +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 features must be validated first on expendable hardware before any irreversible production configuration is recommended. +[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. ### Phase 10 — Authenticated OTA and rollback @@ -233,7 +226,7 @@ Use the existing dual 4 MiB application slots and `otadata` partition to add saf Planned work: -- Accept only authenticated, integrity-checked firmware images compatible with the selected secure-boot policy. +- Accept only authenticated, integrity-checked firmware images under an independently defined OTA signing-trust policy without secure boot; define trusted-key provisioning, rotation, revocation and recovery. This cannot prevent physical firmware replacement. - Support an update path that does not expose a plaintext management endpoint. - Stream downloads with bounded RAM use and explicit progress/error reporting. - Preserve serial, Wi-Fi, HTTPS, and SSH configuration across successful updates. @@ -257,7 +250,7 @@ Planned work: - Preserve binary transparency through explicit framing and MTU-aware partial transfers. - Bound connections, queues, retries, and advertising behavior. - Measure coexistence with 2.4 GHz Wi-Fi, HTTPS, WebSocket, SSH, USB CDC, and UART service. -- Store bond/provisioning data under the hardened secret-storage policy. +- Define bond/provisioning secret retention under the unencrypted-storage threat model; do not assume Phase 9 provides at-rest encryption. - Provide UART0 controls and a physical recovery path for clearing BLE state. BLE remains subordinate to stable Wi-Fi and serial operation; it should be omitted if coexistence or memory costs cannot be bounded acceptably. @@ -303,10 +296,10 @@ Every phase should satisfy the following before being marked complete: The following are not implemented merely because flash partitions or library support exist: -- NVS, flash, or PSRAM encryption. -- Secure boot or production eFuse provisioning. +- Secure boot and encrypted NVS are excluded by explicit user preference, not scheduled future enablement. +- Flash/PSRAM encryption and production eFuse provisioning are outside the current scope, with no enablement commitment. - OTA download, image confirmation, or rollback policy. -- Core-dump collection or secret-safe core-dump processing. +- Core-dump collection or export: the supported Phase 9A baseline disables new dumps; existing partition bytes are not cleared. - Filesystem mounting. - SFTP, SCP, SSH `exec`, forwarding, or subsystems. diff --git a/docs/security_hardening.md b/docs/security_hardening.md new file mode 100644 index 0000000..ec178ff --- /dev/null +++ b/docs/security_hardening.md @@ -0,0 +1,71 @@ +# 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. + +## Scope and threat model + +Reduce network abuse, accidental diagnostic disclosure and unnecessary secret retention while preserving one UART1 broker writer, isolated observers and binary transparency. UART0 remains trusted physical administration/recovery; native USB remains network-independent UART1 access, not an admin console. Whole-device reboot interrupts every transport. + +Secure boot and encrypted NVS are explicitly excluded by user preference. Physical flash/RAM extraction and firmware replacement remain outside the threat model even after Phase 9. There is no commitment to flash/PSRAM encryption, eFuse provisioning or physical JTAG restrictions. Software debugger-aware configuration checks do not disable physical debug access by fuse. + +9A does not change partitions, at-rest encryption, dependencies or generated assets, and requires no upload or erase as part of host/build validation. The unused `nvs_key` and `coredump` partitions remain for layout compatibility. Disabling new dumps does not clear old coredump contents. Logical NVS replacement, reset and credential rotation are not secure erasure; historical plaintext copies can remain. + +## 9A supported build baseline + +`src/security_build_policy.c` enforces the following at compile time, with explicit settings in `sdkconfig.defaults`: + +- Require `CONFIG_ESP_COREDUMP_ENABLE_TO_NONE=y` and `CONFIG_ESP_SYSTEM_PANIC_SILENT_REBOOT=y`. +- Reject enabled core-dump support, flash or UART dump destinations. +- Reject panic print/reboot, print/halt and GDBstub modes, plus runtime GDBstub. +- Reject `CONFIG_ESP_DEBUG_OCDAWARE` and `CONFIG_FREERTOS_DEBUG_OCDAWARE` when enabled. + +The host matrix in `tests/security_build_policy/run.py` compiles the actual guard against synthetic configurations; it is not merely a text check of defaults. Existing generated SDK configuration must also satisfy the guard: defaults alone are not evidence of the effective build configuration. + +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. + +## Operational profiles + +These are handling and validation profiles of the **same supported build baseline**, not separate PlatformIO environments or selectable security overrides. + +| Profile | Operational rules | +|---|---| +| Development | Keep the guard enabled; use synthetic credentials and controlled serial payloads for fault investigation. Keep UART0 recovery available. Review captures before sharing. | +| Test | Use an isolated, expendable target and synthetic secrets; record exact source/configuration, host/build results and device observations. Exercise crashes and recovery without exporting raw memory. | +| Production | Use the same guard, restrict physical/network access, verify device identity through trusted UART0, and apply reviewed provisioning/rotation/recovery procedures. Readiness remains pending Phase 9 review and target evidence. | + +If richer crash debugging is essential, use an isolated synthetic-secret build outside this supported baseline. It requires explicit reviewed changes to the source policy and applicable configuration; no bypass flag is provided. Do not use real credentials or deploy that build as production firmware. Restore and revalidate the supported policy before release. + +Raw flash, RAM and dumps can contain Wi-Fi passwords, private keys, password verifiers, session material and serial payloads. Treat them as secret-bearing and **do not export them as routine diagnostics**. Prefer bounded status/counter observations and reviewed synthetic-secret reproductions. Restrict any exceptional artifacts and define retention/deletion before collecting them; deletion is not a secure-erase guarantee. + +## Validation gates + +### Host and build — passed 2026-09-15 + +From the repository root: + +```sh +python3 tests/security_build_policy/run.py +pio run +python3 tests/security_build_policy/run.py --sdkconfig-header .pio/build/esp32-s3-devkitc-1-n16r8/config/sdkconfig.h +``` + +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. + +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 + +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. + +## 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. +- **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. + +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. diff --git a/sdkconfig.defaults b/sdkconfig.defaults index c47df79..1dd658a 100644 --- a/sdkconfig.defaults +++ b/sdkconfig.defaults @@ -16,7 +16,8 @@ CONFIG_ESP_WIFI_RX_BA_WIN=6 CONFIG_LWIP_TCP_OOSEQ_MAX_PBUFS=4 # Keep concurrent HTTPS handshakes from exhausting scarce internal DRAM. -# Active TLS material remains unencrypted in PSRAM until the hardening phase. +# Active TLS material remains unencrypted in PSRAM; physical extraction is outside +# the supported threat model (docs/security_hardening.md). CONFIG_MBEDTLS_EXTERNAL_MEM_ALLOC=y # CONFIG_MBEDTLS_INTERNAL_MEM_ALLOC is not set # Hardware AES can hang in the PSRAM DMA path and cannot be shared safely with @@ -72,3 +73,16 @@ CONFIG_ESP_WIFI_SOFTAP_SAE_SUPPORT=y # Keep diagnostic and interactive-console logging concise but useful. CONFIG_LOG_DEFAULT_LEVEL_INFO=y + +# Crash memory/registers can contain credentials and serial payloads. Keep the +# reserved coredump partition unused; enforce resolved settings at compile time. +CONFIG_ESP_COREDUMP_ENABLE_TO_NONE=y +# CONFIG_ESP_COREDUMP_ENABLE_TO_FLASH is not set +# CONFIG_ESP_COREDUMP_ENABLE_TO_UART is not set +CONFIG_ESP_SYSTEM_PANIC_SILENT_REBOOT=y +# CONFIG_ESP_SYSTEM_PANIC_PRINT_HALT is not set +# CONFIG_ESP_SYSTEM_PANIC_PRINT_REBOOT is not set +# CONFIG_ESP_SYSTEM_PANIC_GDBSTUB is not set +# CONFIG_ESP_SYSTEM_GDBSTUB_RUNTIME is not set +# CONFIG_ESP_DEBUG_OCDAWARE is not set +# CONFIG_FREERTOS_DEBUG_OCDAWARE is not set diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 5c1b2ca..199234a 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -6,6 +6,7 @@ idf_component_register( "network_console.c" "system_console.c" "secure_random.c" + "security_build_policy.c" "status_led.c" "local_display.c" "local_boot_animation.c" diff --git a/src/security_build_policy.c b/src/security_build_policy.c new file mode 100644 index 0000000..55bbd87 --- /dev/null +++ b/src/security_build_policy.c @@ -0,0 +1,30 @@ +#include "sdkconfig.h" + +/* Check resolved configuration, not just defaults: existing sdkconfig files + * survive default changes. Crash memory may contain credentials and UART data. + * See docs/security_hardening.md before changing this supported-build policy. */ +#if !defined(CONFIG_ESP_COREDUMP_ENABLE_TO_NONE) || !CONFIG_ESP_COREDUMP_ENABLE_TO_NONE +#error "Security policy: select CONFIG_ESP_COREDUMP_ENABLE_TO_NONE=y" +#endif + +#if CONFIG_ESP_COREDUMP_ENABLE_TO_FLASH || CONFIG_ESP_COREDUMP_ENABLE_TO_UART || CONFIG_ESP_COREDUMP_ENABLE +#error "Security policy: flash and UART core dumps must be disabled" +#endif + +#if !defined(CONFIG_ESP_SYSTEM_PANIC_SILENT_REBOOT) || !CONFIG_ESP_SYSTEM_PANIC_SILENT_REBOOT +#error "Security policy: select CONFIG_ESP_SYSTEM_PANIC_SILENT_REBOOT=y" +#endif + +#if CONFIG_ESP_SYSTEM_PANIC_PRINT_HALT || CONFIG_ESP_SYSTEM_PANIC_PRINT_REBOOT || CONFIG_ESP_SYSTEM_PANIC_GDBSTUB +#error "Security policy: panic register output and panic GDB stub must be disabled" +#endif + +#if CONFIG_ESP_SYSTEM_GDBSTUB_RUNTIME +#error "Security policy: runtime GDB stub must be disabled" +#endif + +/* Prevent a connected debugger from replacing panic recovery with a halt. + * This is not a physical JTAG access restriction and does not change eFuses. */ +#if CONFIG_ESP_DEBUG_OCDAWARE || CONFIG_FREERTOS_DEBUG_OCDAWARE +#error "Security policy: JTAG/OCD-aware panic handling must be disabled" +#endif diff --git a/tests/security_build_policy/README.md b/tests/security_build_policy/README.md new file mode 100644 index 0000000..295039f --- /dev/null +++ b/tests/security_build_policy/README.md @@ -0,0 +1,17 @@ +# Crash/debug build-policy regression + +Run from the repository root: + +```sh +python3 tests/security_build_policy/run.py +pio run +python3 tests/security_build_policy/run.py --sdkconfig-header .pio/build/esp32-s3-devkitc-1-n16r8/config/sdkconfig.h +``` + +Requires Python 3.9+ and a host C compiler (`cc`, or `CC`). If a compiler wrapper cannot write its cache in a sandbox, prefix the Python commands with `CCACHE_DISABLE=1`. + +The harness compiles the actual `src/security_build_policy.c`, not a reimplementation. It checks 17 cases: safe undefined/zero disabled booleans, absent configuration, each absent/zero required setting, each forbidden setting enabled independently, and the tracked defaults. It also checks production CMake registration. The optional generated-header check adds an eighteenth case and must follow a successful firmware build; a stale header is not evidence of current firmware configuration. + +Expected-invalid fixtures must fail with the policy's diagnostic. Compiler execution failures or unrelated diagnostics do not count as successful negative tests. No firmware is flashed and no secrets, partition contents or device memory are collected. + +These tests do not exercise actual panic output, reboot/recovery behavior, physical debugging or log redaction. See [Phase 9 policy and target gates](../../docs/security_hardening.md). diff --git a/tests/security_build_policy/run.py b/tests/security_build_policy/run.py new file mode 100644 index 0000000..ab02051 --- /dev/null +++ b/tests/security_build_policy/run.py @@ -0,0 +1,88 @@ +#!/usr/bin/env python3 +"""Compile the production crash-policy guard against safe and unsafe configs.""" +import argparse +import os +from pathlib import Path +import shlex +import subprocess +import tempfile + +ROOT = Path(__file__).resolve().parents[2] +SOURCE = ROOT / "src/security_build_policy.c" +REQUIRED = ( + "CONFIG_ESP_COREDUMP_ENABLE_TO_NONE", + "CONFIG_ESP_SYSTEM_PANIC_SILENT_REBOOT", +) +FORBIDDEN = ( + "CONFIG_ESP_COREDUMP_ENABLE_TO_FLASH", + "CONFIG_ESP_COREDUMP_ENABLE_TO_UART", + "CONFIG_ESP_COREDUMP_ENABLE", + "CONFIG_ESP_SYSTEM_PANIC_PRINT_HALT", + "CONFIG_ESP_SYSTEM_PANIC_PRINT_REBOOT", + "CONFIG_ESP_SYSTEM_PANIC_GDBSTUB", + "CONFIG_ESP_SYSTEM_GDBSTUB_RUNTIME", + "CONFIG_ESP_DEBUG_OCDAWARE", + "CONFIG_FREERTOS_DEBUG_OCDAWARE", +) + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--sdkconfig-header", type=Path, + help="also check an actual generated sdkconfig.h") + args = parser.parse_args() + compiler = shlex.split(os.environ.get("CC", "cc")) + count = 0 + + with tempfile.TemporaryDirectory(prefix="security-build-policy-") as directory: + header = Path(directory) / "sdkconfig.h" + + def check(name, config, accepted): + nonlocal count + header.write_text(config) + result = subprocess.run( + compiler + ["-std=c11", "-Wall", "-Wextra", "-Werror", + "-fsyntax-only", "-I", directory, str(SOURCE)], + capture_output=True, text=True, timeout=20, + ) + if (result.returncode == 0) != accepted: + raise AssertionError(f"{name}: unexpected compiler result\n{result.stderr}") + if not accepted and "Security policy:" not in result.stderr: + raise AssertionError(f"{name}: failed for an unrelated reason\n{result.stderr}") + count += 1 + + def defines(values): + return "".join(f"#define {name} {value}\n" for name, value in values.items()) + + baseline = dict.fromkeys(REQUIRED, 1) + check("supported baseline", defines(baseline), True) + check("explicit disabled options", defines(baseline | dict.fromkeys(FORBIDDEN, 0)), True) + check("missing config", "", False) + for name in REQUIRED: + missing = baseline.copy() + del missing[name] + check(f"missing {name}", defines(missing), False) + check(f"disabled {name}", defines(baseline | {name: 0}), False) + for name in FORBIDDEN: + check(f"enabled {name}", defines(baseline | {name: 1}), False) + + # Verify tracked defaults select the policy, rather than just accepting + # a synthetic fixture. Disabled Kconfig booleans are absent from headers. + defaults = (ROOT / "sdkconfig.defaults").read_text() + values = {} + for line in defaults.splitlines(): + if line.startswith("CONFIG_") and "=" in line: + name, value = line.split("=", 1) + if name in REQUIRED + FORBIDDEN: + values[name] = 1 if value == "y" else 0 + check("tracked defaults", defines(values), True) + if '"security_build_policy.c"' not in (ROOT / "src/CMakeLists.txt").read_text(): + raise AssertionError("production build does not register policy source") + if args.sdkconfig_header: + check("resolved SDK configuration", args.sdkconfig_header.read_text(), True) + + print(f"PASS: {count} compile-policy cases; production source registered") + + +if __name__ == "__main__": + main()