feat(wake): W7 (vi) fold in W6 monitor-integration hardening in beacon.sh
Some checks failed
ci/woodpecker/pr/ci Pipeline failed
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:
@@ -72,6 +72,26 @@ SEQ_FILE="$BEACON_DIR/seq"
|
|||||||
# default keeps the primitive self-contained + testable.
|
# default keeps the primitive self-contained + testable.
|
||||||
RECEIVED_FILE="${WAKE_BEACON_RECEIVED:-$BEACON_DIR/received.json}"
|
RECEIVED_FILE="${WAKE_BEACON_RECEIVED:-$BEACON_DIR/received.json}"
|
||||||
|
|
||||||
|
# The non-circular HMAC signer (A5/W3). Reused VERBATIM here for beacon
|
||||||
|
# authentication (W7 monitor-integration hardening): emit signs the beacon core
|
||||||
|
# via `sign.sh sign-digest`, record verifies via the existing `sign.sh verify`
|
||||||
|
# path. Both resolve the key BY NAME (load_credentials store) — never inline.
|
||||||
|
SIGN_SH="$SCRIPT_DIR/sign.sh"
|
||||||
|
|
||||||
|
# Beacon HMAC key NAME (by-name, never the key itself). Signing is engaged ONLY
|
||||||
|
# when a key name is configured — WAKE_BEACON_HMAC_KEY_NAME, else WAKE_HMAC_KEY_NAME.
|
||||||
|
# When configured, record REQUIRES an authentic, bound envelope (a spoofed or
|
||||||
|
# unsigned beacon is REJECTED). When unset, the legacy unsigned path is preserved
|
||||||
|
# (W7 install-validate is what guarantees a key is configured in production).
|
||||||
|
_beacon_key_name() { printf '%s' "${WAKE_BEACON_HMAC_KEY_NAME:-${WAKE_HMAC_KEY_NAME:-}}"; }
|
||||||
|
|
||||||
|
# Canonical beacon CORE (stdin: a full beacon record; stdout: sorted-key JSON with
|
||||||
|
# the signature envelope and the monitor-stamped ingested_ts stripped). emit signs
|
||||||
|
# this exact projection; record recomputes it identically, so the MAC binds the
|
||||||
|
# beacon's payload and an envelope cannot be lifted onto a different beacon.
|
||||||
|
_beacon_core() { jq -cS 'del(.beacon_envelope) | del(.ingested_ts)'; }
|
||||||
|
_beacon_sha256() { openssl dgst -sha256 -r 2>/dev/null | awk '{print $1}'; }
|
||||||
|
|
||||||
_need_jq() {
|
_need_jq() {
|
||||||
command -v jq >/dev/null 2>&1 || {
|
command -v jq >/dev/null 2>&1 || {
|
||||||
echo "beacon.sh: jq is required" >&2
|
echo "beacon.sh: jq is required" >&2
|
||||||
@@ -215,6 +235,22 @@ cmd_emit() {
|
|||||||
independence:$independence, degraded:$degraded}
|
independence:$independence, degraded:$degraded}
|
||||||
+ (if $supervision_root == "" then {} else {supervision_root:$supervision_root} end)')"
|
+ (if $supervision_root == "" then {} else {supervision_root:$supervision_root} end)')"
|
||||||
|
|
||||||
|
# HMAC-SIGN the beacon (W7 monitor-integration hardening) when a key name is
|
||||||
|
# configured. The signer is the existing non-circular sign.sh, keyed BY NAME —
|
||||||
|
# emit inlines no key. The signed envelope binds the beacon core (seq/emit_ts/
|
||||||
|
# content_hash), so the off-host monitor can reject a spoofed beacon at record.
|
||||||
|
local beacon_key; beacon_key="$(_beacon_key_name)"
|
||||||
|
if [ -n "$beacon_key" ]; then
|
||||||
|
command -v openssl >/dev/null 2>&1 || { echo "beacon.sh: openssl is required to HMAC-sign the beacon (key '$beacon_key' configured)." >&2; exit 3; }
|
||||||
|
local core envelope
|
||||||
|
core="$(printf '%s' "$record" | _beacon_core)"
|
||||||
|
if ! envelope="$(printf '%s' "$core" | "$SIGN_SH" sign-digest --key-name "$beacon_key" --observed-seq "$seq" --emit-ts "$emit_ts" 2>/dev/null)" || [ -z "$envelope" ]; then
|
||||||
|
echo "beacon.sh: FAIL LOUD — could not HMAC-sign the beacon with key '$beacon_key' (resolve it by-name in the credential store). Refusing to emit an UNSIGNED beacon when signing is configured." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
record="$(printf '%s' "$record" | jq -c --argjson env "$envelope" '. + {beacon_envelope:$env}')"
|
||||||
|
fi
|
||||||
|
|
||||||
# DEGRADED beacons are FLAGGED loudly (§1.3) — an isolated host must never
|
# DEGRADED beacons are FLAGGED loudly (§1.3) — an isolated host must never
|
||||||
# silently present a weaker different-supervision-root leg as full independence.
|
# silently present a weaker different-supervision-root leg as full independence.
|
||||||
if [ "$degraded" -eq 1 ]; then
|
if [ "$degraded" -eq 1 ]; then
|
||||||
@@ -262,6 +298,38 @@ cmd_record() {
|
|||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
|
# HMAC-VERIFY the beacon (W7 monitor-integration hardening) — additive to the
|
||||||
|
# integer/monotonicity validation above. When a key name is configured, a beacon
|
||||||
|
# is accepted ONLY if it carries an envelope that is (1) AUTHENTIC under the
|
||||||
|
# by-name key (via the existing sign.sh verify path) and (2) BOUND to THIS beacon
|
||||||
|
# (its signed content_hash == sha256(core) and its seq/emit_ts match). A spoofed
|
||||||
|
# or unsigned beacon is REJECTED, so a forged liveness signal cannot roll the
|
||||||
|
# monitor's dead-man clock forward.
|
||||||
|
local beacon_key; beacon_key="$(_beacon_key_name)"
|
||||||
|
if [ -n "$beacon_key" ]; then
|
||||||
|
command -v openssl >/dev/null 2>&1 || { echo "beacon.sh record: openssl is required to HMAC-verify the beacon (key '$beacon_key' configured)." >&2; exit 3; }
|
||||||
|
local env
|
||||||
|
env="$(printf '%s' "$incoming" | jq -c '.beacon_envelope // empty' 2>/dev/null)"
|
||||||
|
if [ -z "$env" ]; then
|
||||||
|
echo "beacon.sh record: FAIL LOUD — beacon carries no signature envelope but signing is configured (key '$beacon_key'). Rejecting an UNSIGNED/spoofed beacon." >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
if ! printf '%s' "$env" | "$SIGN_SH" verify --key-name "$beacon_key" >/dev/null 2>&1; then
|
||||||
|
echo "beacon.sh record: FAIL LOUD — beacon signature is NOT authentic under key '$beacon_key' (bad HMAC). Rejecting a spoofed beacon." >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
local core core_sha env_chash env_seq env_ts
|
||||||
|
core="$(printf '%s' "$incoming" | _beacon_core)"
|
||||||
|
core_sha="$(printf '%s' "$core" | _beacon_sha256)"
|
||||||
|
env_chash="$(printf '%s' "$env" | jq -r '.signed.content_hash // ""')"
|
||||||
|
env_seq="$(printf '%s' "$env" | jq -r '.signed.observed_seq // ""')"
|
||||||
|
env_ts="$(printf '%s' "$env" | jq -r '.signed.emit_ts // ""')"
|
||||||
|
if [ "$core_sha" != "$env_chash" ] || [ "$env_seq" != "$in_seq" ] || [ "$env_ts" != "$in_ts" ]; then
|
||||||
|
echo "beacon.sh record: FAIL LOUD — beacon signature is valid but NOT bound to this beacon (envelope lifted onto altered fields). Rejecting a spoofed beacon." >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
mkdir -p "$(dirname "$RECEIVED_FILE")"
|
mkdir -p "$(dirname "$RECEIVED_FILE")"
|
||||||
# MONOTONIC receiver: keep the highest-seq beacon. A lower-or-equal seq is a
|
# MONOTONIC receiver: keep the highest-seq beacon. A lower-or-equal seq is a
|
||||||
# stale/replayed beacon and is IGNORED (does not roll the liveness clock back).
|
# stale/replayed beacon and is IGNORED (does not roll the liveness clock back).
|
||||||
@@ -274,7 +342,12 @@ cmd_record() {
|
|||||||
echo "beacon.sh record: ignoring stale/replayed beacon seq $in_seq (<= recorded $last_seq)" >&2
|
echo "beacon.sh record: ignoring stale/replayed beacon seq $in_seq (<= recorded $last_seq)" >&2
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
printf '%s\n' "$incoming" | _atomic_write "$RECEIVED_FILE"
|
# Stamp a MONITOR-SIDE ingested_ts at receive time (W7 monitor-integration
|
||||||
|
# hardening). Staleness (check) is computed from THIS receive-time, never the
|
||||||
|
# host-supplied emit_ts — so a host shipping a far-future emit_ts to defer its
|
||||||
|
# own staleness cannot fool the off-host dead-man clock.
|
||||||
|
local ingested_ts; ingested_ts="$(date +%s)"
|
||||||
|
printf '%s' "$incoming" | jq -c --argjson t "$ingested_ts" '. + {ingested_ts:$t}' | _atomic_write "$RECEIVED_FILE"
|
||||||
}
|
}
|
||||||
|
|
||||||
# --- off-host monitor side: the DEAD-MAN / ABSENCE check -------------------
|
# --- off-host monitor side: the DEAD-MAN / ABSENCE check -------------------
|
||||||
@@ -342,16 +415,20 @@ cmd_check() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
local last_ts last_seq host_id age
|
local last_ts last_seq host_id age
|
||||||
last_ts="$(jq -r '.emit_ts // empty' "$RECEIVED_FILE" 2>/dev/null)"
|
# Staleness clock = the MONITOR's receive-time (ingested_ts), stamped by record.
|
||||||
|
# Fall back to emit_ts only for a legacy record written before ingested_ts
|
||||||
|
# existed. A host-supplied far-future emit_ts therefore CANNOT defer staleness:
|
||||||
|
# once ingested_ts is present it governs, and emit_ts is never used for age.
|
||||||
|
last_ts="$(jq -r '.ingested_ts // .emit_ts // empty' "$RECEIVED_FILE" 2>/dev/null)"
|
||||||
last_seq="$(jq -r '.beacon_seq // empty' "$RECEIVED_FILE" 2>/dev/null)"
|
last_seq="$(jq -r '.beacon_seq // empty' "$RECEIVED_FILE" 2>/dev/null)"
|
||||||
host_id="$(jq -r '.host_id // "unknown"' "$RECEIVED_FILE" 2>/dev/null)"
|
host_id="$(jq -r '.host_id // "unknown"' "$RECEIVED_FILE" 2>/dev/null)"
|
||||||
case "$last_ts" in
|
case "$last_ts" in
|
||||||
'' | *[!0-9]*)
|
'' | *[!0-9]*)
|
||||||
local payload
|
local payload
|
||||||
payload="$(jq -cn --argjson now "$now" --argjson slo "$slo" \
|
payload="$(jq -cn --argjson now "$now" --argjson slo "$slo" \
|
||||||
'{kind:"beacon-absence-alarm", reason:"received beacon has no valid emit_ts (corrupt)",
|
'{kind:"beacon-absence-alarm", reason:"received beacon has no valid receive timestamp (corrupt)",
|
||||||
detected_ts:$now, slo_seconds:$slo}')"
|
detected_ts:$now, slo_seconds:$slo}')"
|
||||||
_fire_alarm "corrupt received beacon (no emit_ts)" "$payload"
|
_fire_alarm "corrupt received beacon (no ingested_ts/emit_ts)" "$payload"
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
@@ -370,7 +447,7 @@ cmd_check() {
|
|||||||
--argjson age "$age" \
|
--argjson age "$age" \
|
||||||
--argjson slo "$slo" \
|
--argjson slo "$slo" \
|
||||||
'{kind:"beacon-absence-alarm", reason:"beacon stale past SLO",
|
'{kind:"beacon-absence-alarm", reason:"beacon stale past SLO",
|
||||||
host_id:$host_id, last_beacon_seq:$last_seq, last_emit_ts:$last_ts,
|
host_id:$host_id, last_beacon_seq:$last_seq, last_received_ts:$last_ts,
|
||||||
detected_ts:$now, age_seconds:$age, slo_seconds:$slo}')"
|
detected_ts:$now, age_seconds:$age, slo_seconds:$slo}')"
|
||||||
_fire_alarm "beacon stale (${age}s > SLO ${slo}s), host=$host_id last_seq=${last_seq:-?}" "$payload"
|
_fire_alarm "beacon stale (${age}s > SLO ${slo}s), host=$host_id last_seq=${last_seq:-?}" "$payload"
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -271,6 +271,66 @@ echo "== B10: fresh beacon within SLO -> ALIVE (exit 0, no alarm) =="
|
|||||||
[ ! -s "$ALARM_OUT" ] || fail_msg "B10: a fresh beacon must NOT fire an alarm"
|
[ ! -s "$ALARM_OUT" ] || fail_msg "B10: a fresh beacon must NOT fire an alarm"
|
||||||
) && ok
|
) && ok
|
||||||
|
|
||||||
|
# ── W7 monitor-integration hardening (folded-in W6 observations, #910 review) ──
|
||||||
|
|
||||||
|
echo "== B11: staleness from monitor ingested_ts -> a far-future emit_ts STILL goes stale =="
|
||||||
|
(
|
||||||
|
H="$(fresh_home b11)"
|
||||||
|
export ALARM_OUT="$H/alarm.json"
|
||||||
|
export WAKE_BEACON_RECEIVED="$H/received.json"
|
||||||
|
export WAKE_ALARM_SINK_CMD="$CAPTURE_ALARM"
|
||||||
|
now="$(date +%s)"
|
||||||
|
# A host ships a FAR-FUTURE emit_ts (now + 100000s) to try to defer staleness.
|
||||||
|
# record stamps the monitor's own ingested_ts; check must use THAT, not emit_ts.
|
||||||
|
jq -cn --argjson ts "$((now + 100000))" \
|
||||||
|
'{kind:"wake-beacon", beacon_seq:9, emit_ts:$ts, host_id:"h", independence:"off-host", degraded:false}' \
|
||||||
|
| "$BEACON" record
|
||||||
|
# Simulate the monitor having received it 100s ago (ingested_ts backdated) while
|
||||||
|
# the host's emit_ts stays far in the future. Under emit_ts-based staleness this
|
||||||
|
# would read as ALIVE (age hugely negative); under receive-time it is STALE.
|
||||||
|
jq -c --argjson ing "$((now - 100))" '.ingested_ts = $ing' "$WAKE_BEACON_RECEIVED" >"$H/tmp" && mv "$H/tmp" "$WAKE_BEACON_RECEIVED"
|
||||||
|
out="$("$BEACON" check --slo-seconds 5 2>&1)"; rc=$?
|
||||||
|
[ "$rc" -eq 1 ] || fail_msg "B11: a far-future emit_ts must NOT defer staleness — receive-time governs (expected absence exit 1); got $rc [$out]"
|
||||||
|
echo "$out" | grep -qi 'ALARM FIRED' || fail_msg "B11: the receive-time-stale beacon must fire the absence alarm [$out]"
|
||||||
|
jq -e '.age_seconds >= 100' "$ALARM_OUT" >/dev/null 2>&1 || fail_msg "B11: staleness age must be measured from ingested_ts (>=100s), not emit_ts [$(cat "$ALARM_OUT" 2>/dev/null)]"
|
||||||
|
) && ok
|
||||||
|
|
||||||
|
echo "== B12: HMAC-verify at record -> a spoofed (bad-sig) beacon is REJECTED =="
|
||||||
|
(
|
||||||
|
H="$(fresh_home b12)"
|
||||||
|
export WAKE_STATE_HOME="$H"
|
||||||
|
export MOSAIC_CREDENTIALS_FILE="$H/credentials.json"
|
||||||
|
export WAKE_BEACON_HMAC_KEY_NAME="beacon"
|
||||||
|
jq -cn --arg k "test-beacon-hmac-key-do-not-echo" '{wake:{hmac_keys:{"beacon":$k}}}' >"$MOSAIC_CREDENTIALS_FILE"
|
||||||
|
export WAKE_BEACON_INDEPENDENCE="off-host"
|
||||||
|
# A capturing sink that keeps the exact shipped (signed) beacon record.
|
||||||
|
SHIPPED="$H/shipped.json"
|
||||||
|
SINK="$H/sink.sh"; printf '#!/usr/bin/env bash\ncat >"%s"\n' "$SHIPPED" >"$SINK"; chmod +x "$SINK"
|
||||||
|
export WAKE_BEACON_SINK_CMD="$SINK"
|
||||||
|
export WAKE_BEACON_RECEIVED="$H/received.json"
|
||||||
|
"$BEACON" emit >/dev/null 2>&1 || fail_msg "B12: a signed emit must succeed with a by-name key"
|
||||||
|
jq -e '.beacon_envelope' "$SHIPPED" >/dev/null 2>&1 || fail_msg "B12: a configured key must produce a signed beacon_envelope [$(cat "$SHIPPED" 2>/dev/null)]"
|
||||||
|
# (a) the authentic signed beacon is ACCEPTED at record.
|
||||||
|
"$BEACON" record <"$SHIPPED" >/dev/null 2>&1 || fail_msg "B12: an authentic signed beacon must be accepted at record"
|
||||||
|
[ -s "$WAKE_BEACON_RECEIVED" ] || fail_msg "B12: the authentic beacon must be stored"
|
||||||
|
# (b) a SPOOFED beacon (fields altered, envelope lifted) is REJECTED.
|
||||||
|
rm -f "$WAKE_BEACON_RECEIVED"
|
||||||
|
spoof="$H/spoof.json"
|
||||||
|
jq -c '.beacon_seq = 99999 | .host_id = "attacker"' "$SHIPPED" >"$spoof"
|
||||||
|
out="$("$BEACON" record <"$spoof" 2>&1)"; rc=$?
|
||||||
|
[ "$rc" -ne 0 ] || fail_msg "B12: a spoofed beacon (altered fields) must be REJECTED at record; got rc=$rc [$out]"
|
||||||
|
echo "$out" | grep -qi 'spoofed beacon' || fail_msg "B12: the rejection must name the spoof [$out]"
|
||||||
|
[ ! -s "$WAKE_BEACON_RECEIVED" ] || fail_msg "B12: a rejected spoof must NOT be stored (dead-man clock not advanced)"
|
||||||
|
# (c) an UNSIGNED beacon is rejected when signing is configured.
|
||||||
|
unsigned="$H/unsigned.json"
|
||||||
|
jq -c 'del(.beacon_envelope)' "$SHIPPED" >"$unsigned"
|
||||||
|
out2="$("$BEACON" record <"$unsigned" 2>&1)"; rc2=$?
|
||||||
|
[ "$rc2" -ne 0 ] || fail_msg "B12: an unsigned beacon must be REJECTED when signing is configured; got rc=$rc2 [$out2]"
|
||||||
|
# beacon.sh must not inline the key material.
|
||||||
|
grep -qF "test-beacon-hmac-key-do-not-echo" "$BEACON" && fail_msg "B12: beacon.sh must NOT inline the HMAC key"
|
||||||
|
true
|
||||||
|
) && ok
|
||||||
|
|
||||||
echo
|
echo
|
||||||
if [ -s "$FAILFILE" ]; then
|
if [ -s "$FAILFILE" ]; then
|
||||||
echo "wake beacon harness: FAILED ($(grep -c . "$FAILFILE") assertion(s))" >&2
|
echo "wake beacon harness: FAILED ($(grep -c . "$FAILFILE") assertion(s))" >&2
|
||||||
|
|||||||
@@ -18,10 +18,11 @@
|
|||||||
# wake-install.sh inlines NO secret/endpoint (v)
|
# wake-install.sh inlines NO secret/endpoint (v)
|
||||||
# I6 reset->verify->retire lifecycle — overlap leaves the legacy timer running;
|
# I6 reset->verify->retire lifecycle — overlap leaves the legacy timer running;
|
||||||
# only a §4-vector pass retires it, and only snapshot-guarded (iii)
|
# only a §4-vector pass retires it, and only snapshot-guarded (iii)
|
||||||
#
|
# I7 ingested_ts staleness — staleness is computed from the monitor's
|
||||||
# The W6 monitor-integration hardening folded into beacon.sh (ingested_ts
|
# receive-time, so a host shipping a FAR-FUTURE emit_ts STILL goes stale (vi-a)
|
||||||
# staleness + beacon HMAC-verify at record) is exercised by test-wake-beacon.sh
|
# I8 beacon HMAC-verify at record — a spoofed (bad-sig) beacon is REJECTED (vi-b)
|
||||||
# and appended here as I7/I8 in the same change that lands that beacon hardening.
|
# (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,
|
# 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
|
# 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)"
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
WI="$SCRIPT_DIR/wake-install.sh"
|
WI="$SCRIPT_DIR/wake-install.sh"
|
||||||
|
BEACON="$SCRIPT_DIR/beacon.sh"
|
||||||
FRAMEWORK_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
FRAMEWORK_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
||||||
|
|
||||||
command -v jq >/dev/null 2>&1 || { echo "SKIP: jq not available" >&2; exit 0; }
|
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"
|
[ -f "$SNAP/$TIMER" ] || fail_msg "I6: the snapshot must survive the retire"
|
||||||
) && ok
|
) && 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
|
echo
|
||||||
if [ -s "$FAILFILE" ]; then
|
if [ -s "$FAILFILE" ]; then
|
||||||
echo "wake install harness: FAILED ($(grep -c . "$FAILFILE") assertion(s))" >&2
|
echo "wake install harness: FAILED ($(grep -c . "$FAILFILE") assertion(s))" >&2
|
||||||
|
|||||||
Reference in New Issue
Block a user