Blueforce-Linux/tests/test-field-os-lifecycle.sh
emirhanmamak-blueforce ef102b9657 feat(field-os): provisioning layer, real ISO build, offline contracts
Adds the Blueforce Field OS provisioning stack on top of the existing fleet
architecture, without rewriting the working systems (installer, diagnostics,
Ansible, WireGuard/SSH/xRDP/RustDesk, Docker policies, monitoring, Docusaurus).

ISO build (the previous builder was a skeleton that produced no medium):
- boot layout is read from the base ISO at build time via
  `xorriso -report_el_torito as_mkisofs` and rebuilt with `-as mkisofs`;
  `-boot_image any replay` cannot work on Ubuntu live ISOs because the GRUB2
  MBR lives in the source image's first 16 sectors and the EFI system partition
  is an appended partition, neither of which is a file in the ISO9660 tree
- preflight now asserts El Torito boot metadata positively: xorriso exits 0 on
  a non-ISO input, which previously let an arbitrary file pass --check
- artifact produced and verified: dist/Blueforce-Field-OS-1.0.0-amd64.iso,
  6 481 917 952 bytes, SHA256 cb0cc56f...d431296, BIOS+UEFI entries preserved,
  /autoinstall.yaml at the media root, casper/minimal.squashfs byte-identical
  to the base
- recorded baseline deviation: the verified medium on the build host is the
  Desktop flavour, so the manifest carries a baseline_deviation block instead
  of hiding it; Server remains one flag away

Offline and identity:
- offline APT contract with concrete package requirements, pin format and an
  audit table; builds fail closed while pins are UNPINNED
- autoinstall keeps interactive-sections: [storage]: the operator confirms the
  target disk, and no automatic wipe is ever performed
- firstboot terminal wizard for the 8-digit dealer id, three-state lifecycle
  (PROVISIONED_OFFLINE -> ENROLLED -> READY) with fail-closed gates, so a
  missing internet link or WireGuard handshake cannot fail provisioning but can
  never produce a false READY

Fleet and migration:
- five read-only Ansible playbooks for Field OS version, provisioning,
  enrollment, remote channels and offline readiness, all wave-scoped with a
  separate production override
- Windows pre-migration inventory and data export extended to the full brief
  scope, read-only, never collecting credentials

Tests: tests/run-all.sh -> 8/8 pass, exit 0. Boot acceptance (UEFI, Legacy
BIOS, Secure Boot), air-gapped install, real enrollment endpoint and Windows
rollback remain LAB work and are listed in IMPLEMENTATION-REPORT.md.
2026-09-16 11:14:42 +03:00

396 lines
28 KiB
Bash
Executable file

#!/usr/bin/env bash
# Exercise the Field OS lifecycle, enrollment and readiness contracts with mocked local services.
#
# This host has no systemd, no apt and no internet, so every external dependency
# (systemctl, wg, ip, dpkg-query, curl) is a mock in $TMP/bin. Scenarios:
#
# 1 secret-safe enrollment request/header/response path
# 2 offline installer fails closed without a local repository
# 3 failed WireGuard activation rolls back and keeps the prior phase
# 4 ROLLBACK/READY: independent proof, fail-closed without central evidence
# 5 skeleton release manifest is never a production release
# 6 state machine: forward-only, no stage skipping, contract validation
# 7 bf-check-local passes without internet and fails closed on key hygiene
# 8 bf-check-enrollment requires central evidence, peer config and handshake
# 9 replay/backwards protection and atomic 0600 state writes
# 10 systemd attempt: PENDING without a token, bounded retry with one staged
# 11 --check dry run never mutates state or sends a request
# 12 enrollment schemas match the real client behaviour
# 13 boot unit cannot loop, carries no credential and stays bounded
set -euo pipefail
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
BIN="$REPO_ROOT/scripts/diagnostics"
TMP="$(mktemp -d)"
trap 'rm -rf "$TMP"' EXIT
fail() { printf 'FAIL: %s\n' "$*" >&2; exit 1; }
pass() { printf 'OK: %s\n' "$*"; }
PUBLIC_KEY='AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA='
SERVER_KEY='BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB='
DEALER='12345678'
ENDPOINT='https://control.blueforce.test/enroll'
make_mock_bin() {
local d="$1"
mkdir -p "$d"
cat > "$d/systemctl" <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
printf '%s\n' "$*" >> "$MOCK_CALLS"
case "${1:-}" in
is-active) [[ "${MOCK_SYSTEMCTL_FAIL:-0}" != 1 ]] ;;
is-enabled) [[ "${MOCK_SYSTEMCTL_FAIL:-0}" != 1 ]] ;;
enable|restart) [[ "${MOCK_SYSTEMCTL_FAIL:-0}" != 1 ]] ;;
get-default) printf 'multi-user.target\n' ;;
*) exit 0 ;;
esac
EOF
cat > "$d/curl" <<'EOF'
#!/usr/bin/env bash
# Mock curl: records argv, captures the request file and the header fd, then
# either writes the canned response or fails like a transport/protocol error.
set -euo pipefail
request='' output='' header='' code=''
while [[ $# -gt 0 ]]; do
case "$1" in
--data-binary) request="$2"; shift 2 ;;
--output) output="$2"; shift 2 ;;
--write-out) code="$2"; shift 2 ;;
-H|--header) header="$2"; shift 2 ;;
*) printf 'argv=%s\n' "$1" >> "$MOCK_CURL_LOG"; shift ;;
esac
done
[[ "$request" == @* ]] || exit 91
cat "${request#@}" > "$MOCK_REQUEST_CAPTURE"
[[ "$header" == @* ]] || exit 92
cat "${header#@}" > "$MOCK_HEADER_CAPTURE"
if [[ "${MOCK_CURL_FAIL:-0}" == 1 ]]; then
printf '%s' "${MOCK_HTTP_CODE:-000}"
exit "${MOCK_CURL_RC:-7}"
fi
printf '%s' "$MOCK_RESPONSE" > "$output"
[[ -z "$code" ]] || printf '%s' "${MOCK_HTTP_CODE:-200}"
EOF
cat > "$d/wg" <<'EOF'
#!/usr/bin/env bash
case "${1:-}" in
show) printf 'peer %s\n' "${MOCK_HANDSHAKE:-0}" ;;
pubkey) IFS= read -r _key || true; printf '%s\n' "${MOCK_PUBLIC_KEY:-}" ;;
*) exit 0 ;;
esac
EOF
cat > "$d/ip" <<'EOF'
#!/usr/bin/env bash
[[ "${MOCK_IP_FAIL:-0}" == 1 ]] && exit 1
exit 0
EOF
cat > "$d/dpkg-query" <<'EOF'
#!/usr/bin/env bash
printf 'install ok installed'
EOF
chmod +x "$d"/*
}
MOCK_BIN="$TMP/bin"; make_mock_bin "$MOCK_BIN"
export PATH="$MOCK_BIN:$PATH" MOCK_CALLS="$TMP/calls" MOCK_CURL_LOG="$TMP/curl-argv"
export MOCK_REQUEST_CAPTURE="$TMP/request" MOCK_HEADER_CAPTURE="$TMP/header"
export MOCK_PUBLIC_KEY="$PUBLIC_KEY"
STATE_DIR="$TMP/state"; WG_DIR="$TMP/wg"; mkdir -p "$STATE_DIR" "$WG_DIR"
printf '%s\n' "$DEALER" > "$STATE_DIR/dealer-id"
printf '%s\n' 'private-test-key' > "$WG_DIR/privatekey"
printf '%s\n' "$PUBLIC_KEY" > "$WG_DIR/publickey"
chmod 600 "$WG_DIR/privatekey" "$WG_DIR/publickey" "$STATE_DIR/dealer-id"
export STATE_DIR WG_DIR BF_STATE_FILE="$STATE_DIR/state.json"
IDEM="$(python3 -c "import hashlib,sys;print(hashlib.sha256(('p-1|BF-'+sys.argv[1]+'|'+sys.argv[2]).encode()).hexdigest())" "$DEALER" "$PUBLIC_KEY")"
write_state() { printf '%s\n' "$1" > "$BF_STATE_FILE"; chmod 600 "$BF_STATE_FILE"; }
state_field() { python3 -c "import json,sys;print(json.load(open(sys.argv[1])).get(sys.argv[2]))" "$BF_STATE_FILE" "$1"; }
central_pending='{"status":"pending","verification_id":"cv-1","monitoring":{"status":"pending","verification_id":"mon-1"},"remote":{"status":"pending","verification_id":"rem-1"},"management":{"status":"pending","verification_id":"mgmt-1"}}'
central_verified='{"status":"verified","verification_id":"cv-1","monitoring":{"status":"verified","verification_id":"mon-1"},"remote":{"status":"verified","verification_id":"rem-1"},"management":{"status":"verified","verification_id":"mgmt-1"}}'
pending_state="{\"phase\":\"PROVISIONED_OFFLINE\",\"enrollment_status\":\"PENDING\",\"fleet_status\":\"PENDING\",\"device_id\":\"BF-$DEALER\",\"provisioning_id\":\"p-1\"}"
enrolled_state() { printf '{"phase":"ENROLLED","enrollment_status":"ENROLLED","fleet_status":"PENDING","device_id":"BF-%s","provisioning_id":"p-1","central_verification":%s}' "$DEALER" "$1"; }
ready_state() { printf '{"phase":"READY","enrollment_status":"ENROLLED","fleet_status":"READY","device_id":"BF-%s","provisioning_id":"p-1","central_verification":%s}' "$DEALER" "$1"; }
mocked_response() { printf '{"status":"accepted","device_id":"BF-%s","idempotency_key":"%s","enrollment_id":"enr-1","wireguard":{"address":"10.20.0.17/32","endpoint":"wg.blueforce.test:51820","server_public_key":"%s","allowed_ips":"10.20.0.0/24"},"central_verification":%s}' "$DEALER" "$IDEM" "$SERVER_KEY" "$1"; }
# --- scenario 1: the secret-safe enrollment path ----------------------------
write_state "$pending_state"
export MOCK_RESPONSE="$(mocked_response "$central_verified")"
printf '%s\n' 'super-secret-token' | BF_ENROLLMENT_ENDPOINT="$ENDPOINT" "$BIN/bf-enroll" --token-stdin >/dev/null
[[ "$(state_field phase)" == 'ENROLLED' ]] || fail 'successful enrollment must commit ENROLLED'
[[ -s "$WG_DIR/wg0.conf" ]] || fail 'successful enrollment must install wg0.conf'
[[ "$(stat -c '%a' "$WG_DIR/wg0.conf")" == '600' ]] || fail 'wg0.conf must be mode 600'
[[ "$(stat -c '%a' "$BF_STATE_FILE")" == '600' ]] || fail 'state.json must be mode 600'
grep -q "BF-$DEALER" "$MOCK_REQUEST_CAPTURE" || fail 'curl must consume the generated request file'
grep -Fqx 'Authorization: Bearer super-secret-token' "$MOCK_HEADER_CAPTURE" || fail 'curl must receive the exact supplied token through process substitution'
! grep -R -F 'super-secret-token' "$TMP" --exclude=header --exclude=request --exclude=enrollment.token || fail 'token persisted outside the transient curl fixture'
! grep -R -F 'super-secret-token' "$MOCK_CURL_LOG" "$STATE_DIR" 2>/dev/null || fail 'token leaked to curl argv, output, or state'
pass 'curl request/header/response path is secret-safe and functional'
# --- scenario 12 (early check): request body carries public facts only -------
python3 - "$MOCK_REQUEST_CAPTURE" "$PUBLIC_KEY" <<'PY'
import json, sys
request = json.load(open(sys.argv[1], encoding='utf-8'))
if set(request) != {'device_id', 'wireguard_public_key', 'idempotency_key', 'hardware', 'release'}:
raise SystemExit('unexpected request field set: %s' % sorted(request))
if request['wireguard_public_key'] != sys.argv[2]:
raise SystemExit('request must carry the local public key')
blob = json.dumps(request).lower()
for banned in ('private', 'secret', 'password', 'passwd', 'bearer', 'authorization'):
if banned in blob:
raise SystemExit('request carries a secret-like field: ' + banned)
PY
! grep -q 'private-test-key' "$MOCK_REQUEST_CAPTURE" || fail 'the WireGuard private key must never be sent'
! grep -q 'super-secret-token' "$MOCK_REQUEST_CAPTURE" || fail 'the one-time token must never be sent in the request body'
pass 'request carries public facts only: no private key, no token, no credential'
# --- scenario 2: offline installer fails closed -----------------------------
cat > "$MOCK_BIN/apt-get" <<'EOF'
#!/usr/bin/env bash
printf '%s\n' "$*" >> "$MOCK_APT_CALLS"
exit 90
EOF
chmod +x "$MOCK_BIN/apt-get"
export MOCK_APT_CALLS="$TMP/apt-calls"
if STATE_DIR="$TMP/offline-state" LOG_FILE="$TMP/offline.log" BF_OFFLINE_REPO="$TMP/missing-offline-repo" "$REPO_ROOT/scripts/install/blueforce-install.sh" --offline --dealer-id "$DEALER" --only 02-system --yes > "$TMP/offline.out" 2>&1; then fail 'offline installer must reject a missing local repository before modules'; fi
[[ ! -e "$MOCK_APT_CALLS" ]] || fail 'offline repository rejection must not invoke apt-get'
! grep -Eqi '(curl|wget|https?://|apt[[:space:]]+download)' "$TMP/offline.out" "$TMP/offline.log" 2>/dev/null || fail 'offline rejection must not run or report network package acquisition'
pass 'offline installer fails closed before package modules when local repository is absent'
# --- scenario 3: activation failure rolls back, phase is preserved ----------
write_state "$pending_state"
rm -f "$WG_DIR/wg0.conf"
export MOCK_SYSTEMCTL_FAIL=1
if printf '%s\n' 'another-token' | BF_ENROLLMENT_ENDPOINT="$ENDPOINT" "$BIN/bf-enroll" --token-stdin; then fail 'activation failure must fail enrollment'; fi
[[ "$(state_field phase)" == 'PROVISIONED_OFFLINE' ]] || fail 'activation failure must not commit ENROLLED'
[[ "$(state_field enrollment_status)" == 'PENDING' ]] || fail 'activation failure must leave enrollment PENDING'
[[ ! -e "$WG_DIR/wg0.conf" ]] || fail 'activation failure must roll back a new config'
[[ -z "$(find "$STATE_DIR" -name '.state.json.*' -print -quit)" ]] || fail 'atomic state writes must leave no temporary files'
unset MOCK_SYSTEMCTL_FAIL
pass 'state transition rolls back on WireGuard activation failure and stays PENDING'
# --- scenario 4: READY proof is independent and fails closed ----------------
printf '%s\n' 'third-token' | BF_ENROLLMENT_ENDPOINT="$ENDPOINT" "$BIN/bf-enroll" --token-stdin >/dev/null
[[ -s "$WG_DIR/wg0.conf" ]] || fail 're-enrollment after a rolled-back attempt must install wg0.conf'
RUST_SYSTEM="$TMP/RustDesk-system.toml"; RUST_USER="$TMP/RustDesk-user.toml"
printf "rendezvous-server = 'rustdesk.blueforce.test'\nrelay-server = 'rustdesk.blueforce.test'\n" > "$RUST_SYSTEM"
cp "$RUST_SYSTEM" "$RUST_USER"
export MOCK_HANDSHAKE="$(date +%s)" BF_RUSTDESK_SYSTEM_CONFIG="$RUST_SYSTEM" BF_RUSTDESK_USER_CONFIG="$RUST_USER" BF_MESH_CONFIG="$TMP/no-mesh"
python3 - "$BF_STATE_FILE" "$central_verified" <<'PY'
import json, sys
path, verified = sys.argv[1], json.loads(sys.argv[2])
state = json.load(open(path, encoding='utf-8'))
state['central_verification'] = verified
json.dump(state, open(path, 'w', encoding='utf-8'))
PY
"$BIN/bf-check-ready" >/dev/null || fail 'ENROLLED state with independent local and central evidence must become ready-check eligible'
python3 - "$BF_STATE_FILE" <<'PY'
import json, sys
path = sys.argv[1]
state = json.load(open(path, encoding='utf-8'))
del state['central_verification']
json.dump(state, open(path, 'w', encoding='utf-8'))
PY
if "$BIN/bf-check-ready" > "$TMP/ready-failclosed.out" 2>&1; then fail 'missing central verification evidence must fail closed'; fi
if "$BIN/bf-check-enrollment" > "$TMP/enroll-failclosed.out" 2>&1; then fail 'the ENROLLED gate must fail closed without central evidence'; fi
grep -q 'central_verification is missing' "$TMP/enroll-failclosed.out" || fail 'the ENROLLED gate must name the missing central evidence'
pass 'READY proof is independent of persisted READY and fails closed without central evidence'
# --- scenario 5: skeleton release manifest ----------------------------------
RELEASE="$TMP/release.yaml"
printf 'release:\n name: blueforce-provisioning-skeleton\n version: 0.1.0\n status: skeleton\n commit: deadbeef\n' > "$RELEASE"
if BF_RELEASE_MANIFEST="$RELEASE" "$BIN/bf-release" > "$TMP/release.out"; then fail 'skeleton release must be nonzero'; fi
grep -q 'UNAPPROVED_SKELETON' "$TMP/release.out" || fail 'skeleton release must be labelled unapproved'
pass 'nested skeleton manifest is not represented as a production release'
# --- scenario 6: state machine contract -------------------------------------
write_state "$pending_state"
[[ "$("$BIN/bf-enrollment-status" --field phase)" == 'PROVISIONED_OFFLINE' ]] || fail 'canonical reader must report the provisioned phase'
write_state "$(enrolled_state "$central_pending")"
"$BIN/bf-enrollment-status" | grep -qx 'Enrollment: ENROLLED' || fail 'ENROLLED stage must be visible before READY'
write_state "$(ready_state "$central_verified")"
"$BIN/bf-enrollment-status" > "$TMP/ready-status.out" || fail 'READY with verified central evidence must be contract-valid'
grep -qx 'Lifecycle: READY' "$TMP/ready-status.out" || fail 'READY stage must be reported'
grep -qx 'Enrollment: COMPLETE' "$TMP/ready-status.out" || fail 'READY implies a complete enrollment'
grep -qx 'Fleet: READY' "$TMP/ready-status.out" || fail 'READY implies fleet READY'
write_state "{\"phase\":\"READY\",\"enrollment_status\":\"PENDING\",\"fleet_status\":\"READY\",\"device_id\":\"BF-$DEALER\",\"provisioning_id\":\"p-1\"}"
if "$BIN/bf-enrollment-status" > "$TMP/skip.out" 2>/dev/null; then fail 'a skipped stage (PROVISIONED_OFFLINE -> READY) must be INVALID'; fi
grep -qx 'Lifecycle: INVALID' "$TMP/skip.out" || fail 'skipped stage must be reported as INVALID'
write_state "$(ready_state "$central_pending")"
if "$BIN/bf-enrollment-status" >/dev/null 2>&1; then fail 'READY without verified channels must be INVALID'; fi
write_state "{\"phase\":\"PROVISIONED_OFFLINE\",\"enrollment_status\":\"PENDING\",\"fleet_status\":\"PENDING\",\"device_id\":\"BF-$DEALER\"}"
if "$BIN/bf-enrollment-status" >/dev/null 2>&1; then fail 'a state without provisioning_id must be INVALID'; fi
if BF_STATE_FILE="$TMP/absent.json" "$BIN/bf-enrollment-status" > "$TMP/absent.out" 2>/dev/null; then fail 'a missing state file must exit non-zero'; fi
grep -qx 'Lifecycle: PENDING' "$TMP/absent.out" || fail 'a missing state file must report PENDING'
pass 'state machine is forward-only, never skips a stage and validates its contract'
# --- scenario 6b: bf-status lifecycle block ---------------------------------
write_state "$(enrolled_state "$central_pending")"
BF_STATE_FILE="$BF_STATE_FILE" "$BIN/bf-status" > "$TMP/status.out" 2>/dev/null || fail 'bf-status must always exit 0'
grep -qx 'Provisioning : COMPLETE' "$TMP/status.out" || fail 'bf-status must show Provisioning : COMPLETE once central accepted the device'
grep -qx 'Enrollment : ENROLLED' "$TMP/status.out" || fail 'bf-status must show Enrollment : ENROLLED'
grep -qx 'Fleet State : NOT READY' "$TMP/status.out" || fail 'bf-status must show Fleet State : NOT READY before READY'
write_state "$pending_state"
"$BIN/bf-status" > "$TMP/status-pending.out" 2>/dev/null
grep -qx 'Provisioning : PROVISIONED_OFFLINE' "$TMP/status-pending.out" || fail 'bf-status must show the offline provisioning stage'
grep -qx 'Enrollment : PENDING' "$TMP/status-pending.out" || fail 'bf-status must show a pending enrollment'
write_state "$(ready_state "$central_verified")"
"$BIN/bf-status" > "$TMP/status-ready.out" 2>/dev/null
grep -qx 'Fleet State : READY' "$TMP/status-ready.out" || fail 'bf-status must show Fleet State : READY'
write_state "{\"phase\":\"READY\",\"enrollment_status\":\"PENDING\",\"fleet_status\":\"READY\",\"device_id\":\"BF-$DEALER\",\"provisioning_id\":\"p-1\"}"
"$BIN/bf-status" > "$TMP/status-invalid.out" 2>/dev/null
grep -q 'lifecycle state invalid' "$TMP/status-invalid.out" || fail 'bf-status must raise a critical alert for an invalid lifecycle state'
pass 'bf-status prints the Provisioning/Enrollment/Fleet State lifecycle block'
# --- scenario 7: local gate is offline-safe ---------------------------------
write_state "$pending_state"
rm -f "$WG_DIR/wg0.conf" "$STATE_DIR/enrollment.token"
"$BIN/bf-check-local" > "$TMP/local.out" 2>&1 || fail 'bf-check-local must pass offline with no handshake and no wg0.conf'
grep -q 'local gates PASS' "$TMP/local.out" || fail 'bf-check-local must report a passing local gate'
! grep -q '^FAIL' "$TMP/local.out" || fail 'bf-check-local must not fail any local gate offline'
"$BIN/bf-check-local" --check >/dev/null 2>&1 || fail 'bf-check-local --check must behave like the default run'
[[ ! -e "$WG_DIR/wg0.conf" ]] || fail 'bf-check-local must not create enrollment artifacts'
! grep -Eq '(^|/)(curl|wget|apt|apt-get)( |$)' "$TMP/calls" 2>/dev/null || fail 'bf-check-local must not call network tools'
chmod 644 "$WG_DIR/privatekey"
if "$BIN/bf-check-local" >/dev/null 2>&1; then fail 'a world-readable WireGuard private key must fail the local gate'; fi
chmod 600 "$WG_DIR/privatekey"
MOCK_PUBLIC_KEY='CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC=' "$BIN/bf-check-local" >/dev/null 2>&1 && fail 'a mismatched WireGuard keypair must fail the local gate'
if BF_STATE_FILE="$TMP/absent.json" "$BIN/bf-check-local" >/dev/null 2>&1; then fail 'a missing state file must fail the local gate'; fi
pass 'bf-check-local passes offline and fails closed on key hygiene'
# --- scenario 8: ENROLLED gate needs evidence and a live tunnel -------------
write_state "$pending_state"
rm -f "$WG_DIR/wg0.conf"
printf '%s\n' 'enrolled-token' | BF_ENROLLMENT_ENDPOINT="$ENDPOINT" "$BIN/bf-enroll" --token-stdin >/dev/null
export MOCK_HANDSHAKE="$(date +%s)"
[[ -s "$WG_DIR/wg0.conf" ]] || fail 'a successful enrollment must install the assigned peer configuration'
[[ "$(state_field phase)" == 'ENROLLED' ]] || fail 'a successful enrollment must commit ENROLLED'
"$BIN/bf-check-enrollment" >/dev/null || fail 'ENROLLED gates must pass with peer config, live tunnel and a fresh handshake'
MOCK_HANDSHAKE=0 "$BIN/bf-check-enrollment" >/dev/null 2>&1 && fail 'a missing WireGuard handshake must fail the ENROLLED gate'
export MOCK_HANDSHAKE="$(date +%s)"
MOCK_IP_FAIL=1 "$BIN/bf-check-enrollment" >/dev/null 2>&1 && fail 'a missing wg0 interface must fail the ENROLLED gate'
MOCK_IP_FAIL=1 "$BIN/bf-check-ready" >/dev/null 2>&1 && fail 'a missing wg0 interface must fail the READY gate'
mv "$WG_DIR/wg0.conf" "$TMP/wg0.conf.keep"
"$BIN/bf-check-enrollment" >/dev/null 2>&1 && fail 'a missing peer configuration must fail the ENROLLED gate'
mv "$TMP/wg0.conf.keep" "$WG_DIR/wg0.conf"
write_state "$pending_state"
"$BIN/bf-check-enrollment" >/dev/null 2>&1 && fail 'PROVISIONED_OFFLINE must never pass the ENROLLED gate'
write_state "$(enrolled_state "$central_pending")"
"$BIN/bf-check-enrollment" >/dev/null || fail 'a device with pending central channels is still ENROLLED'
if "$BIN/bf-check-ready" >/dev/null 2>&1; then fail 'pending central channels must never produce READY'; fi
pass 'bf-check-enrollment requires central evidence, peer configuration and a fresh handshake'
# --- scenario 9: replay and backwards protection ----------------------------
write_state "$pending_state"
printf '%s\n' 'replay-token' | BF_ENROLLMENT_ENDPOINT="$ENDPOINT" "$BIN/bf-enroll" --token-stdin >/dev/null
REPLAY_BEFORE="$(python3 -c "import json,sys;print(json.dumps(json.load(open(sys.argv[1])),sort_keys=True))" "$BF_STATE_FILE")"
if printf '%s\n' 'replay-token' | BF_ENROLLMENT_ENDPOINT="$ENDPOINT" "$BIN/bf-enroll" --token-stdin > "$TMP/replay.out" 2>&1; then fail 'a second enrollment without --reenroll must be refused'; fi
grep -q 'already ENROLLED' "$TMP/replay.out" || fail 'the replay refusal must explain the ENROLLED state'
REPLAY_AFTER="$(python3 -c "import json,sys;print(json.dumps(json.load(open(sys.argv[1])),sort_keys=True))" "$BF_STATE_FILE")"
[[ "$REPLAY_BEFORE" == "$REPLAY_AFTER" ]] || fail 'a refused replay must not touch the state file'
printf '%s\n' 'replay-token' | BF_ENROLLMENT_ENDPOINT="$ENDPOINT" "$BIN/bf-enroll" --token-stdin --reenroll >/dev/null || fail '--reenroll must re-apply enrollment'
[[ "$(state_field phase)" == 'ENROLLED' ]] || fail '--reenroll must never move the phase backwards'
write_state "$(ready_state "$central_verified")"
if printf '%s\n' 'replay-token' | BF_ENROLLMENT_ENDPOINT="$ENDPOINT" "$BIN/bf-enroll" --token-stdin --reenroll > "$TMP/ready-refusal.out" 2>&1; then fail 'a READY device must refuse enrollment even with --reenroll'; fi
grep -q 'never move backwards' "$TMP/ready-refusal.out" || fail 'the READY refusal must explain the forward-only rule'
[[ "$(state_field phase)" == 'READY' ]] || fail 'the READY refusal must not change the phase'
[[ "$(stat -c '%a' "$BF_STATE_FILE")" == '600' ]] || fail 'state writes must stay at mode 600'
[[ -z "$(find "$STATE_DIR" -name '.state.json.*' -print -quit)" ]] || fail 'atomic writes must not leave temporary files'
pass 'replay and backwards transitions are refused, state writes are atomic and 0600'
# --- scenario 10: systemd attempt and staged token --------------------------
write_state "$pending_state"
rm -f "$WG_DIR/wg0.conf" "$STATE_DIR/enrollment.token"
BF_ENROLLMENT_ENDPOINT="$ENDPOINT" "$BIN/bf-enroll" --systemd > "$TMP/pending.out" 2>&1 || fail 'the boot attempt must exit 0 while nothing is staged'
grep -q 'PENDING' "$TMP/pending.out" || fail 'the boot attempt must report PENDING'
[[ ! -e "$WG_DIR/wg0.conf" ]] || fail 'a pending boot attempt must not install a config'
BF_STATE_FILE="$TMP/absent.json" BF_ENROLLMENT_ENDPOINT="$ENDPOINT" "$BIN/bf-enroll" --systemd > "$TMP/unprovisioned.out" 2>&1 || fail 'an unprovisioned device must exit 0 so the unit never loops'
grep -q 'PENDING' "$TMP/unprovisioned.out" || fail 'an unprovisioned device must report PENDING'
BF_ENROLLMENT_ENDPOINT="$ENDPOINT" "$BIN/bf-enroll" --systemd >/dev/null 2>&1 || fail 'a boot attempt without central configuration must exit 0'
printf '%s\n' 'staged-secret-token' | BF_ENROLLMENT_ENDPOINT="$ENDPOINT" "$BIN/bf-enroll" --token-stdin --stage >/dev/null || fail '--stage must store the token'
[[ "$(stat -c '%a' "$STATE_DIR/enrollment.token")" == '600' ]] || fail 'the staged token must be mode 600'
MOCK_CURL_FAIL=1 MOCK_CURL_RC=7 BF_ENROLLMENT_ENDPOINT="$ENDPOINT" "$BIN/bf-enroll" --systemd > "$TMP/offline-attempt.out" 2>&1 && fail 'an unreachable endpoint must fail the attempt so the bounded retry can run'
[[ -e "$STATE_DIR/enrollment.token" ]] || fail 'a transient failure must keep the staged token for the bounded retry'
[[ "$(state_field phase)" == 'PROVISIONED_OFFLINE' ]] || fail 'a failed attempt must keep the lifecycle at PROVISIONED_OFFLINE'
[[ ! -e "$WG_DIR/wg0.conf" ]] || fail 'a failed attempt must not leave a WireGuard config behind'
unset MOCK_CURL_FAIL MOCK_CURL_RC
MOCK_CURL_FAIL=1 MOCK_CURL_RC=22 MOCK_HTTP_CODE=403 BF_ENROLLMENT_ENDPOINT="$ENDPOINT" "$BIN/bf-enroll" --systemd >/dev/null 2>&1 && fail 'a definitive rejection must fail the attempt'
[[ ! -e "$STATE_DIR/enrollment.token" ]] || fail 'a definitive rejection must discard the consumed token'
unset MOCK_CURL_FAIL MOCK_CURL_RC MOCK_HTTP_CODE
printf '%s\n' 'staged-secret-token-two' | BF_ENROLLMENT_ENDPOINT="$ENDPOINT" "$BIN/bf-enroll" --token-stdin --stage >/dev/null
touch -d '2 hours ago' "$STATE_DIR/enrollment.token"
BF_ENROLLMENT_TOKEN_MAX_AGE=1800 BF_ENROLLMENT_ENDPOINT="$ENDPOINT" "$BIN/bf-enroll" --systemd >/dev/null || fail 'an expired staged token must exit 0 as PENDING'
[[ ! -e "$STATE_DIR/enrollment.token" ]] || fail 'an expired staged token must be discarded'
printf '%s\n' 'staged-secret-token-three' | BF_ENROLLMENT_ENDPOINT="$ENDPOINT" "$BIN/bf-enroll-now" --stage >/dev/null || fail 'the technician wrapper must stage a token'
[[ -e "$STATE_DIR/enrollment.token" ]] || fail 'bf-enroll-now --stage must leave the token staged'
BF_ENROLLMENT_ENDPOINT="$ENDPOINT" "$BIN/bf-enroll" --systemd >/dev/null || fail 'the boot attempt must enroll when a valid token is staged and central is reachable'
[[ "$(state_field phase)" == 'ENROLLED' ]] || fail 'the boot attempt must commit ENROLLED'
[[ ! -e "$STATE_DIR/enrollment.token" ]] || fail 'the staged token must be deleted after use'
! grep -R -F 'staged-secret-token-three' "$TMP" --exclude=header --exclude=request --exclude=enrollment.token || fail 'the staged token must not leak into state, logs or argv'
pass 'the boot attempt stays PENDING without a token and joins automatically with one'
# --- scenario 11: --check dry run -------------------------------------------
write_state "$pending_state"
rm -f "$WG_DIR/wg0.conf" "$STATE_DIR/enrollment.token"
BEFORE="$(cat "$BF_STATE_FILE")"
BF_ENROLLMENT_ENDPOINT="$ENDPOINT" "$BIN/bf-enroll" --token-stdin --check > "$TMP/check.out" 2>&1 || fail '--check must succeed on a valid pre-enrollment device'
grep -q 'would enroll' "$TMP/check.out" || fail '--check must describe the planned transition'
[[ "$BEFORE" == "$(cat "$BF_STATE_FILE")" ]] || fail '--check must not touch the state file'
[[ ! -e "$WG_DIR/wg0.conf" ]] || fail '--check must not write a WireGuard config'
[[ ! -e "$STATE_DIR/enrollment.token" ]] || fail '--check must not stage a token'
"$BIN/bf-enroll" --token-stdin --check >/dev/null 2>&1 && fail '--check must fail without an endpoint'
if BF_ENROLLMENT_ENDPOINT='https://example.invalid/enroll' "$BIN/bf-enroll" --token-stdin --check >/dev/null 2>&1; then fail '--check must reject placeholder endpoints'; fi
pass '--check validates without mutating or sending anything'
# --- scenario 12: schemas match the client behaviour ------------------------
python3 - "$REPO_ROOT" "$MOCK_REQUEST_CAPTURE" "$MOCK_RESPONSE" <<'PY'
import json, sys
root, request_capture, response_sample = sys.argv[1:4]
request_schema = json.load(open(root + '/provisioning/enrollment/schemas/enrollment-request.schema.json', encoding='utf-8'))
response_schema = json.load(open(root + '/provisioning/enrollment/schemas/enrollment-response.schema.json', encoding='utf-8'))
request = json.load(open(request_capture, encoding='utf-8'))
response = json.loads(response_sample)
if set(request_schema['properties']) != set(request_schema['required']):
raise SystemExit('request schema must close every property it requires')
if set(request) != set(request_schema['required']):
raise SystemExit('captured request does not match the request schema')
for section, keys in (('hardware', ('machine_id', 'product_uuid', 'sys_vendor')), ('release', ('os_release', 'kernel'))):
if set(request_schema['properties'][section]['required']) != set(keys):
raise SystemExit('%s schema fields drifted' % section)
if set(request[section]) != set(keys):
raise SystemExit('captured %s does not match the schema' % section)
required_response = set(response_schema['required'])
if set(response) - set(response_schema['properties']):
raise SystemExit('canned response carries fields the schema forbids')
if not required_response <= set(response):
raise SystemExit('canned response is missing required fields')
if set(response['wireguard']) != set(response_schema['properties']['wireguard']['required']):
raise SystemExit('WireGuard response fields drifted')
if set(response['central_verification']) != set(response_schema['properties']['central_verification']['required']):
raise SystemExit('central verification fields drifted')
blob = json.dumps(response_schema).lower()
for banned in ('private_key', 'privkey', 'password', 'bearer'):
if banned in blob:
raise SystemExit('response schema must not define a secret field: ' + banned)
PY
! grep -q 'BF-12345678' "$REPO_ROOT/provisioning/enrollment/README.md" || fail 'provisioning docs must not embed a device identity'
pass 'enrollment schemas match the implemented request and response contract'
# --- scenario 13: boot unit cannot loop and carries no credential -----------
UNIT="$REPO_ROOT/config/systemd/blueforce-enroll.service"
TIMER="$REPO_ROOT/config/systemd/blueforce-enroll.timer"
grep -q '^Restart=on-failure$' "$UNIT" || fail 'the boot unit must retry only failures'
grep -q '^Restart=always' "$UNIT" && fail 'the boot unit must never restart unconditionally'
grep -q '^RestartSec=' "$UNIT" || fail 'the boot unit must delay its bounded retries'
grep -q '^StartLimitBurst=' "$UNIT" || fail 'the boot unit must bound its retries'
grep -q '^StartLimitIntervalSec=' "$UNIT" || fail 'the boot unit must bound its retry window'
grep -q 'ExecStart=/usr/local/bin/bf-enroll --systemd' "$UNIT" || fail 'the boot unit must run the controlled attempt'
grep -q 'Unit=blueforce-enroll.service' "$TIMER" || fail 'the timer must trigger the enrollment attempt'
grep -q 'OnBootSec=' "$TIMER" || fail 'the timer must attempt shortly after boot'
! grep -Eqi '(token|password|secret|bearer)[[:space:]]*=' "$UNIT" "$TIMER" || fail 'the units must not embed a credential'
exec_lines="$(grep -E '^Exec[A-Za-z]*=' "$UNIT" || true)"
[[ -n "$exec_lines" ]] || fail 'the boot unit must define its attempt command'
! printf '%s\n' "$exec_lines" | grep -Eqi 'reboot|shutdown' || fail 'the boot unit must never reboot or shut down the device'
grep -q 'never reboots' "$UNIT" || fail 'the boot unit must document that it never reboots the device'
pass 'the enrollment units retry in a bounded way and carry no credential'
printf 'test-field-os-lifecycle: all scenarios passed\n'