feat(wake): W7 (vi) fold in W6 monitor-integration hardening in beacon.sh
Some checks failed
ci/woodpecker/pr/ci Pipeline failed

Separate, clearly-labeled commit for observation set (vi) from the #910 review.
The HMAC-verify half is a non-trivial blast radius (emit must sign, record must
verify-and-bind), so it is isolated here rather than expanding the installer diff.
Both changes are additive and backward-compatible (the legacy unsigned path and
emit_ts fallback are preserved when no key is configured).

(vi-a) Monitor-side ingested_ts: `record` stamps the monitor's own receive-time,
       and `check` computes staleness from ingested_ts (falling back to emit_ts
       only for a legacy record). A host shipping a far-future emit_ts to defer
       its own staleness can no longer fool the off-host dead-man clock.
(vi-b) Beacon HMAC-verify at record: when a key name is configured
       (WAKE_BEACON_HMAC_KEY_NAME / WAKE_HMAC_KEY_NAME), `emit` signs the beacon
       core via the existing sign.sh sign-digest path and `record` verifies via
       sign.sh verify AND binds the envelope to this beacon (content_hash ==
       sha256(core), seq/emit_ts match). A spoofed or unsigned beacon is REJECTED
       fail-loud and never advances the received clock. Key is by-name only —
       beacon.sh inlines no key material.

test-wake-beacon.sh extended with B11 (ingested_ts staleness) and B12 (HMAC-verify
+ spoof/unsigned rejection); test-wake-install.sh gains I7/I8 for the same
invariants. Both harnesses shown red-first under targeted mutation, then green.

Part of #892

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0158NZqN2n2ymKFeJAZ4GUCb
This commit is contained in:
mosaic-coder
2026-07-25 22:07:46 -05:00
parent 231ba2ef32
commit ce53b0e646
3 changed files with 185 additions and 9 deletions

View File

@@ -18,10 +18,11 @@
# wake-install.sh inlines NO secret/endpoint (v)
# I6 reset->verify->retire lifecycle — overlap leaves the legacy timer running;
# only a §4-vector pass retires it, and only snapshot-guarded (iii)
#
# The W6 monitor-integration hardening folded into beacon.sh (ingested_ts
# staleness + beacon HMAC-verify at record) is exercised by test-wake-beacon.sh
# and appended here as I7/I8 in the same change that lands that beacon hardening.
# I7 ingested_ts staleness — staleness is computed from the monitor's
# receive-time, so a host shipping a FAR-FUTURE emit_ts STILL goes stale (vi-a)
# I8 beacon HMAC-verify at record — a spoofed (bad-sig) beacon is REJECTED (vi-b)
# (I7/I8 exercise the W6 monitor-integration hardening folded into beacon.sh; the
# same invariants are also asserted in depth by test-wake-beacon.sh B11/B12.)
#
# Isolated per-group XDG/systemd/target dirs; no live network, no operator queue,
# no real systemd manager touched (the blank-reset verify uses the deterministic
@@ -34,6 +35,7 @@ set -uo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
WI="$SCRIPT_DIR/wake-install.sh"
BEACON="$SCRIPT_DIR/beacon.sh"
FRAMEWORK_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
command -v jq >/dev/null 2>&1 || { echo "SKIP: jq not available" >&2; exit 0; }
@@ -198,6 +200,43 @@ echo "== I6: reset->verify->retire — overlap keeps legacy running; only §4-ve
[ -f "$SNAP/$TIMER" ] || fail_msg "I6: the snapshot must survive the retire"
) && ok
# ── W6 monitor-integration hardening folded into beacon.sh (vi) ────────────────
echo "== I7: ingested_ts staleness — a far-future emit_ts still goes stale (receive-time governs) =="
(
H="$(fresh i7)"
export WAKE_BEACON_RECEIVED="$H/received.json"
export WAKE_ALARM_SINK_CMD="cat >$H/alarm.json"
now="$(date +%s)"
# Host lies with a far-future emit_ts; record stamps the monitor's ingested_ts.
jq -cn --argjson ts "$((now + 100000))" \
'{kind:"wake-beacon", beacon_seq:3, emit_ts:$ts, host_id:"h", independence:"off-host", degraded:false}' \
| bash "$BEACON" record
# Simulate the beacon having been received 100s ago (backdate ingested_ts only).
jq -c --argjson ing "$((now - 100))" '.ingested_ts = $ing' "$WAKE_BEACON_RECEIVED" >"$H/t" && mv "$H/t" "$WAKE_BEACON_RECEIVED"
out="$(bash "$BEACON" check --slo-seconds 5 2>&1)"; rc=$?
[ "$rc" -eq 1 ] || fail_msg "I7: far-future emit_ts must NOT defer staleness — receive-time governs (expected exit 1); got $rc [$out]"
) && ok
echo "== I8: beacon HMAC-verify at record — a spoofed (bad-sig) beacon is REJECTED =="
(
H="$(fresh i8)"
export WAKE_STATE_HOME="$H"
export MOSAIC_CREDENTIALS_FILE="$H/credentials.json"
export WAKE_BEACON_HMAC_KEY_NAME="beacon"
jq -cn --arg k "i8-hmac-key" '{wake:{hmac_keys:{"beacon":$k}}}' >"$MOSAIC_CREDENTIALS_FILE"
export WAKE_BEACON_INDEPENDENCE="off-host"
export WAKE_BEACON_SINK_CMD="cat >$H/shipped.json"
export WAKE_BEACON_RECEIVED="$H/received.json"
bash "$BEACON" emit >/dev/null 2>&1 || fail_msg "I8: signed emit must succeed"
bash "$BEACON" record <"$H/shipped.json" >/dev/null 2>&1 || fail_msg "I8: an authentic signed beacon must be accepted"
rm -f "$WAKE_BEACON_RECEIVED"
jq -c '.beacon_seq = 88888 | .host_id = "attacker"' "$H/shipped.json" >"$H/spoof.json"
out="$(bash "$BEACON" record <"$H/spoof.json" 2>&1)"; rc=$?
[ "$rc" -ne 0 ] || fail_msg "I8: a spoofed beacon must be REJECTED at record (rc=$rc) [$out]"
[ ! -s "$WAKE_BEACON_RECEIVED" ] || fail_msg "I8: a rejected spoof must NOT advance the dead-man clock"
) && ok
echo
if [ -s "$FAILFILE" ]; then
echo "wake install harness: FAILED ($(grep -c . "$FAILFILE") assertion(s))" >&2