Files
ESP32_Serial_Swiss_Army_Knife/tests/web_admin_tickets/README.md
T
Commander1024 aeb2043396 feat: add bounded admin WebSocket backend (Phase 8D.5)
- Require current admin cookie sessions, Origin checks and single-use
  tickets
- Reuse the shared console with session-aware authorization and slot
  allocation
- Add HTTPD-owned I/O, bounded buffering and revocation cleanup
- Prevent LRU eviction of serial clients and stale admin socket closure
- Reject unsupported web-shell mutations before side effects
- Add host regressions, a smoke client and resource accounting

Validated by user sign-off after a 15-minute full-client soak at 230400
baud, with a few broker drops under heavy output. Browser UI remains
for Phase 8D.6; numeric memory reserves remain open.
2026-09-06 14:41:41 +02:00

5.2 KiB

Admin ticket store host checks

Run from the repository root:

python3 tests/web_admin_tickets/run.py
python3 tests/web_admin_tickets/run.py --sanitize

Requires a C11 cc, Python 3, OpenSSL development headers/libcrypto, and (for --sanitize) ASan/UBSan runtimes. No firmware build, network, generated assets, or persistent build output. The runner reuses the session-store harness's tiny platform header fakes. test.c includes the unmodified production C, using real project principal/session declarations and OpenSSL SHA-256. Inclusion gives white-box access for wipe, saturation and exhaustion assertions without adding production test hooks. RNG, time and session validation are deterministic fakes; all external calls assert that the ticket critical section is not held.

Exact test groups

  1. Stopped/start/idempotent-start lifecycle; 64 hex output; SHA-256 digest-only storage; success, replay denial and full record wipe.
  2. Two-ticket capacity and no live eviction; exact counters; nested competing issuance takes the last slot and the losing output is wiped.
  3. Issue rejects user role, public-key method, mismatched generation, zero ID, NULL principal/output, stale sessions and session-check errors.
  4. Consume burns matches before denying wrong session, user role, public-key method, generation, stale/check-error, zero ID or NULL principal; also rejects a different session with the same account principal.
  5. Empty/NULL/short/long/nonhex input; uppercase hex consumes the same secret.
  6. Success one microsecond before expiry; rejection at expiry; stale reclaim on issue/snapshot; snapshot expiry cleanup; signed deadline overflow rejection.
  7. Revocation ID precedence, exact username length/name and global scope; revocation never invalidates the fake sessions.
  8. RNG/SHA failures, failed output wipe, consume SHA failure leaves the unidentifiable ticket intact, duplicate live RNG/digest rejection.
  9. Issuance RNG/SHA hooks exercise stop/restart, global and nonmatching revoke, and session invalidation; currentness hook exercises stop/restart.
  10. Consume SHA/postcheck hooks exercise stop/restart, global/nonmatching revoke, stale sessions and expiry; nested competing consumes admit exactly once.
  11. Prune check races replacement with the same ID, digest and deadline; the non-reused record generation protects the replacement from stale cleanup.
  12. Nonwrapping epoch and record generation exhaustion, permanent lifecycle failure at exhaustion, saturated counters, NULL/count-only snapshots and host structure sizes.

Contract and limits

The public API is in src/web_admin_tickets.h. This module is inert until wired by a later integration increment. It adds no routes, session invalidation, transport, task, socket, queue, timer or heap allocation. Callers must authorize HTTP cookie/Origin/CSRF, invalidate the authoritative session store before calling revoke, wipe successful token outputs and recheck currentness at later sensitive boundaries. A successful consume is not an authorization lease.

Two tickets, 32 RNG bytes each, 64 hex characters plus NUL, absolute 30-second lifetime. Only SHA-256 of decoded secret bytes is retained with copied principal, session ID, deadline and unique generation. Both hex cases are accepted. Live digest collisions fail rather than creating ambiguous tickets. No retry loop or live eviction. Pruning checks at most two copied records per invocation. Every revoke advances the epoch even if no record matches, conservatively cancelling unrelated in-flight issue/consume work. Start is idempotent while ready. Stop/start never resets counters, epoch or record generation.

issued counts published tickets, consumed counts burned matches (including subsequently denied admissions), rejected counts failed issue/consume calls; capacity_rejections is a subset of rejected. All counters saturate at UINT32_MAX. Snapshot prunes expired/stale records and exports counts, readiness and storage size only. A capacity failure is ESP_ERR_NO_MEM; malformed input INVALID_ARG; unauthorized/stale/lifecycle-raced work INVALID_STATE; no live consume match NOT_FOUND; SHA failure ESP_FAIL; RNG errors propagate. Failed issue wipes all 65 output bytes when output is non-NULL. Output must not alias inputs.

Host measured sizes: ticket 104 B, two-ticket state 248 B, fake lock 4 B, snapshot 40 B; snapshot storage_bytes = 252 B. Estimated 32-bit target sizes: ticket 96 B, state 232 B, plus the target portMUX (typically 8 B), roughly 240 B static RAM. These are estimates, not target linker measurements. Issue plus nested prune has 240 B of explicit ticket/random local payload on this host (about 224 B on a 32-bit target), excluding scalar/compiler frames and session/RNG/SHA call stacks; caller also owns a 65 B token. No measured target stack/flash delta.

Hooks test deterministic interleavings, not true multicore scheduling or IDF portMUX semantics. They do not validate the real DRBG, mbedTLS, session database, HTTP admission, hardware, or full Phase 8D.5 integration. Post-check account changes without notification are subject to the same no-lease boundary as the session API. Combined hardware validation remains pending; no firmware build or device operation is part of this increment.