Commit Graph

7 Commits

Author SHA1 Message Date
ruv c88b7a12d8 fix(homecore): review findings from PR #1451 — HAP secret redaction, REST history/logbook cap, event_type validation, migration --force
Post-release deep review of the merged HOMECORE platform PR (#1451)
turned up several real issues, fixed here:

- homecore-hap: StoredAccessory's permanent Ed25519 signing seed and
  StoredSetup's SRP salt/verifier were reachable via derived Debug.
  Not actively triggered by any current code path, but a future
  logging/panic-message change (an ordinary thing to add) would have
  printed the accessory's compromise-forever identity key in
  plaintext -- there's no rotation mechanism. Added manual, redacted
  Debug impls matching the existing SetupCode pattern; StoreState/
  PairingStore's derived Debug inherits the redaction automatically.
  New test pins the exact rendered output.

- homecore-api: /api/history/period and /api/logbook rejected the
  default (no filter_entity_id/entity) call shape once a room had
  more than 32 known entities -- exactly how the real HA frontend
  calls these endpoints. The MAX_HISTORY_ENTITIES cap now only
  applies to an explicit, unusually-large filter list; the existing
  MAX_API_HISTORY_ROWS total-row budget already bounds the actual
  work regardless of entity count. Two new tests: unfiltered succeeds
  with 40 known entities, explicit oversized filter is still rejected.

- homecore-api: fire_event (both REST and WS) restricted event_type
  to [a-z0-9_]+, but real HA integrations commonly fire mixed-case,
  dotted, or hyphenated types (mobile_app.notification_action,
  ios.action_fired). Factored the check into a single
  is_valid_event_type() shared by both transports, relaxed to what
  actually matters for server safety: non-empty, length-bounded, no
  control characters.

- homecore-migrate: write_config_entries/write_device_registry/
  write_entity_registry's atomic no-clobber write had no escape
  hatch -- an operator who fixed a bad source row (or wanted a fresh
  re-import) had to manually delete prior output first. Added a
  --force CLI flag (default off, preserving the existing no-clobber
  default and its explicit malformed_entry_is_an_error_not_a_partial_write
  test) that atomically replaces an existing destination. First
  attempt used bare fs::rename, which hit real ERROR_ACCESS_DENIED
  sharing-violation flakiness on Windows; switched to pre-clearing
  the destination then publishing through the same hard_link step
  the default path already uses reliably.

All touched crates re-verified: homecore-hap 46 tests (was 45),
homecore-api 20+6+6+7=39 tests (was 18+6+6+7=37), homecore-migrate 25
tests (was 24), all 0 failed, clippy clean under -D warnings.

Also corrected the public v2051 GitHub release notes: "Wasmtime
36.0.12 component loading" was inaccurate (the crate uses the
core-module API with a hand-rolled host ABI, not the Component
Model/WIT) -- doc-only, not a code change.

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-07-27 16:15:56 -04:00
ruv bc690ff309 feat(homecore-api): serve bounded recorder history 2026-07-27 14:43:48 -04:00
ruv fbd5cfa242 feat(homecore-server): wire HAP runtime and registry APIs 2026-07-27 14:41:25 -04:00
ruv ac1fdfb725 feat(homecore): add API compatibility and voice protocols 2026-07-27 13:59:36 -04:00
rUv 581af67fbc fix(homecore): harden runtime and publish truthful capabilities (#1450) 2026-07-27 10:41:43 -07:00
rUv db3d94a313 fix(homecore-api security): auth-gate GET /api/ (was unauthenticated) + recover WS subscription on broadcast lag (#1076)
* fix(homecore-api security): auth-gate GET /api/ (HC-API-AUTH-01, ADR-161)

`rest::api_root` took no headers and unconditionally returned
`200 {"message":"API running."}`, while every sibling REST route gates
on `BearerAuth::from_headers`. HA's `APIStatusView` inherits
`requires_auth = True`, so `/api/` must return 401 for a missing/wrong
bearer — HA clients use it as a token-validation probe, so a 200 told a
bad-token client its token was valid and let an unauthenticated party
confirm a live endpoint. LOW severity (static body, no data leak),
reported at true severity.

Fix: `api_root(headers, State)` validates the bearer like `get_config`.

Pinned by fails-on-old tests (200 -> assert 401):
- api_root_rejects_missing_bearer
- api_root_rejects_wrong_bearer
guarded by api_root_accepts_correct_bearer (still 200 with valid token).

Co-Authored-By: claude-flow <ruv@ruv.net>

* fix(homecore-api security): recover WS subscription on broadcast lag (HC-WS-LAG-01, ADR-161)

`subscribe_events`'s per-subscription task matched `Err(_) => break` on
both broadcast `recv()` arms. `RecvError::Lagged(n)` (a slow consumer
falling >EVENT_CHANNEL_CAPACITY=4,096 events behind) is recoverable —
the bus doc says "Lagged receivers must re-sync" and HA keeps the
subscription alive across a lag. The old code treated the first lag as
fatal, so after an event burst the client's stream went permanently
silent with no error frame — a self-inflicted event-delivery DoS under
load. LOW severity.

Fix: `Lagged(_) => continue` (skip dropped window, re-sync),
`Closed => break`, on both the system and domain arms.

Pinned by subscription_survives_broadcast_lag: subscribes, floods 6,000
filtered events past the 4,096 capacity to force a Lagged, then asserts
a subsequent subscribed event is still delivered (old code: 5s timeout).

Co-Authored-By: claude-flow <ruv@ruv.net>

* docs(homecore-api security): record HC-API-AUTH-01 + HC-WS-LAG-01 review (ADR-161)

CHANGELOG [Unreleased] Security entry + ADR-161 addendum documenting the
beyond-SOTA network-API review: two LOW bugs fixed (unauthenticated
GET /api/; WS subscription killed on broadcast lag) and the
auth/traversal/injection/info-leak/CORS dimensions confirmed clean with
evidence (no traversal surface — in-memory DashMap + EntityId allowlist;
HashSet token compare, not a byte-== timing oracle).

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-06-14 16:48:57 -04:00
ruv 9d52d49c0b fix(homecore-api): close WS auth bypass + reply-theater, harden dev bin (ADR-161 A1/A2/A8)
A1 (CRITICAL): the /api/websocket handshake accepted any non-empty token,
ignoring the LongLivedTokenStore whitelist the REST path enforces — a full
WS auth bypass. Now validates via state.tokens().is_valid() before auth_ok;
wrong tokens get auth_invalid + close.

A2 (HIGH): WS command replies were pushed into an mpsc whose only consumer
logged and discarded them — no result/pong/event reached the client. Split
the socket with futures StreamExt::split; a dedicated writer task drains the
response channel onto the wire.

A8 (HIGH): the homecore-api dev bin bound 0.0.0.0 with unconditional
allow-any auth and no env path. Wired the HOMECORE_TOKENS env path (dev
fallback warn-logged when unset) and defaulted the bind to 127.0.0.1
(HOMECORE_BIND to opt into LAN).

Tests (fail on old source):
- ws_handshake::wrong_token_is_rejected (old → auth_ok)
- ws_handshake::result_reply_is_received / ping_pong_reply_is_received (old → timeout)
- server_bin_auth::provisioned_bin_rejects_wrong_bearer / from_env_path_enforces_whitelist

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-06-12 00:55:16 -04:00