Add authenticated WebSocket serial terminal - dirty commit with front-
and backend issues
This commit is contained in:
@@ -12,7 +12,7 @@ Universal wireless serial adaptor firmware for the ESP32-S3.
|
||||
- 8 MB octal PSRAM
|
||||
- Adafruit MAX3243 full-pinout RS-232 breakout, product 5988
|
||||
|
||||
The firmware has completed **Phase 0 hardware characterization**, the **Phase 1 serial-core foundation**, the **Phase 2 transport-neutral session broker**, native USB CDC-ACM, and the **Phase 4 Wi-Fi foundation**. The current Phase 5A foundation adds authenticated HTTPS with a persistent device-specific identity and physical-console recovery. WebSocket/xterm.js serial transport is deliberately deferred to the next web phase. The MAX3243 diagnostics and recovery consoles remain available. No electrical test starts automatically; UART1 starts when requested explicitly or when a host opens native USB CDC.
|
||||
The firmware has completed **Phase 0 hardware characterization**, the **Phase 1 serial-core foundation**, the **Phase 2 transport-neutral session broker**, native USB CDC-ACM, the **Phase 4 Wi-Fi foundation**, and the **Phase 5 authenticated HTTPS web terminal**. Phase 5B adds an offline xterm.js interface and bounded WebSocket transport to the persistent HTTPS identity and recovery foundation from Phase 5A. The MAX3243 diagnostics and recovery consoles remain available. No electrical test starts automatically; UART1 starts when requested explicitly or when a native USB or authenticated web-terminal session opens.
|
||||
|
||||
## Hardware wiring
|
||||
|
||||
@@ -273,16 +273,22 @@ Wi-Fi credentials currently reside as plaintext in the application-owned `wifi_a
|
||||
8. Test `wifi stop`, `wifi start`, and `wifi reconnect` while confirming UART0 and native USB serial operation remain unaffected.
|
||||
9. If available, test a WPA3-only profile and a wrong password, then inspect the disconnect reason and counters.
|
||||
|
||||
### Phase 5A authenticated HTTPS foundation
|
||||
### Phase 5 authenticated HTTPS web terminal
|
||||
|
||||
One ESP-IDF HTTPS server listens on TCP port 443 across whichever AP and station interfaces are active. There is no plaintext port 80 listener. This slice intentionally exposes only:
|
||||
One ESP-IDF HTTPS server listens on TCP port 443 across whichever AP and station interfaces are active. There is no plaintext port 80 listener. Phase 5A established persistent authentication, certificate management, and recovery; Phase 5B adds these local-only browser resources and transport endpoints:
|
||||
|
||||
```text
|
||||
GET /
|
||||
GET /api/status
|
||||
GET /
|
||||
GET /api/status
|
||||
POST /api/ws-ticket
|
||||
WSS /ws/serial?ticket=<one-time-ticket>
|
||||
GET /assets/xterm.css
|
||||
GET /assets/xterm.js
|
||||
GET /assets/addon-fit.js
|
||||
GET /assets/app.js
|
||||
```
|
||||
|
||||
Both endpoints require HTTP Basic authentication over TLS. `/` is a small self-contained status landing page; `/api/status` returns JSON containing uptime plus non-secret Wi-Fi, serial-service, broker, native-USB, and HTTPS state/counters. No WebSocket, xterm.js terminal, or web broker client exists in Phase 5A.
|
||||
The page, status API, assets, and ticket endpoint require HTTP Basic authentication over TLS. `/` is now a responsive xterm.js serial workspace; `/api/status` returns JSON containing uptime plus non-secret Wi-Fi, serial-service, broker, native-USB, HTTPS, and WebSocket state/counters. xterm.js and FitAddon are pinned, vendored, compressed, and served by the ESP32 itself, so the terminal works while connected only to the fallback AP and never depends on a CDN.
|
||||
|
||||
On first boot, the device generates and persists:
|
||||
|
||||
@@ -332,7 +338,37 @@ openssl s_client -connect 192.168.4.1:443 -servername esp32-sak-device.local </d
|
||||
|
||||
Replace the example SNI name with the DNS SAN printed by `web certificate info`. SNI is not required for this single-certificate server, but supplying the device name makes the test representative of future hostname use. Repeat the fingerprint check after reboot to confirm persistence, then optionally test each explicit rotation command and verify that only the requested material changes.
|
||||
|
||||
HTTPS is memory-bounded to two simultaneous client sockets; ESP-IDF documents approximately 40 KiB per TLS socket. Basic authentication is acceptable here only because plaintext HTTP is disabled. It is an initial administration mechanism, not the final authorization design.
|
||||
#### WebSocket authentication and broker behavior
|
||||
|
||||
Browser JavaScript cannot reliably attach a Basic `Authorization` header to a WebSocket constructor. The authenticated page therefore obtains a 192-bit random, RAM-only ticket with `POST /api/ws-ticket`, then presents that ticket once in the WSS URL. The server stores only its SHA-256 digest, accepts it once within 30 seconds, binds it to the current credential generation, and creates no broker client until validation succeeds. Credential rotation invalidates outstanding tickets and closes active web-terminal sessions.
|
||||
|
||||
ESP-IDF 5.5 sends the RFC 6455 `101 Switching Protocols` response before invoking the application WebSocket handler. Consequently, an invalid ticket receives the protocol upgrade and is then closed immediately rather than receiving an HTTP `401`; it never gains a broker session, serial output, or writer access. Strict rejection before `101` would require a framework-level pre-handshake authorization hook that ESP-IDF 5.5 does not provide.
|
||||
|
||||
Each accepted browser becomes a normal `SESSION_BROKER_CLIENT_WEB`. Opening the first terminal starts UART1 if necessary and automatically requests the writer lease. A competing web or USB client remains a read-only observer when another client owns the lease. The page clearly reports its role and provides **Request control**, **Release control**, and **Reconnect** actions. Broker ownership remains authoritative even if a browser is stale or malicious.
|
||||
|
||||
Serial traffic uses binary WebSocket frames. Browser input is UTF-8 encoded and split into at most 1024-byte frames; output is drained in at most 512-byte frames. Each web session permits only one queued/in-flight TLS frame. If a browser stops reading, its own 4096-byte broker observer queue eventually drops data without blocking UART reception, USB, or another broker observer. ESP-IDF performs TLS sends on one shared HTTP task, so a slow TLS peer can delay other HTTPS work for at most the configured one-second socket timeout; this is bounded rather than absolute per-socket isolation.
|
||||
|
||||
A direct ticket diagnostic is available without exposing the ticket in firmware logs:
|
||||
|
||||
```sh
|
||||
curl -k -u 'admin:YOUR_24_CHARACTER_PASSWORD' -X POST https://192.168.4.1/api/ws-ticket
|
||||
```
|
||||
|
||||
For end-to-end validation:
|
||||
|
||||
1. Open `https://192.168.4.1/`, accept the device certificate warning, and authenticate as `admin`.
|
||||
2. Confirm xterm.js loads without Internet access and the page reaches **Connected / Writer** when no other writer exists.
|
||||
3. Send text, terminal escape sequences, UTF-8, and pasted input through an RS-232 loopback or peer; verify exact traffic through `web counters`, `broker clients`, and serial counters.
|
||||
4. Open a second browser/private session. It should connect as an observer, receive the same UART output, and keep terminal input disabled.
|
||||
5. Release control in the first browser, request it in the second, and verify the role badges, broker writer ID, and actual serial input ownership change together.
|
||||
6. Open native USB while a web writer exists, then repeat with USB owning the lease. Confirm each losing transport remains an observer and cannot inject bytes.
|
||||
7. Close/reload a browser and verify its broker client disappears, the writer lease is released when applicable, and reconnect uses a fresh ticket.
|
||||
8. Run `web credentials rotate --force`; existing browsers should disconnect and old credentials must no longer mint tickets.
|
||||
9. Exercise `web stop`, `web start`, and reboot while confirming UART0/native USB recovery remains available and no stale web broker clients survive.
|
||||
|
||||
Vendored browser sources, versions, hashes/provenance, deterministic gzip artifacts, and MIT license notices are recorded under [`web_assets/`](web_assets/SOURCES.md). Third-party code remains under its upstream license; project firmware code remains GPL-3.0-only.
|
||||
|
||||
HTTPS permits up to six simultaneous client sockets: two bounded persistent WebSocket terminals plus parallel browser asset, ticket, and status requests. ESP-IDF documents approximately 40 KiB per active TLS socket, so this is a concurrency ceiling rather than preallocated per-socket memory. WebSocket serial sessions themselves remain fixed at two. Basic authentication is acceptable here only because plaintext HTTP is disabled. It is an initial administration mechanism, not the final authorization design.
|
||||
|
||||
**Current security limitation:** the web password and ECDSA private key are stored as plaintext in the application-owned `web_sec/material` NVS blob, just as Wi-Fi credentials are currently plaintext in `wifi_app/config`. The reserved `nvs_key` partition does not activate NVS encryption. ESP-IDF 5.5 also keeps an internal heap copy of the active TLS private key and does not guarantee zeroization when that allocation is freed. Do not treat the current firmware as resistant to physical flash or RAM extraction; NVS encryption, flash encryption, secure boot, protected OTA, secret-aware core-dump handling, and framework-level key zeroization belong to the later hardening phase.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user