Files
takana/scripts/attest-boot-test.sh
T
sergioandClaude Opus 4.8 a44d05e7e2 atestación firmada END-TO-END: hammer firma, arje-zero-attest verifica (I4/D)
Cierra la integración canónica firmada de I4 (decisión del usuario): la mitad de
arje (gate) + la mitad de hammer (firma) acopladas con la cripto real de
arje-attest/agora — NO el manifiesto plano de /ente/attest.json.

tawasuyu (pusheado, commit c78a0ada en main): `arje-packager --seed-out` emite el
seed FIRMADO standalone (el gate attest_gate.rs ya estaba en main).

hammer:
- recipes/arje-packager.toml: el firmador (build-time tool, static musl, corre en
  el host). GOTCHA: lib+bin + monorepo virtual ⇒ flags `-p ... --bin ...`.
- recipes/arje-zero-attest.toml: arje-zero CON gate, variante SÓLO de producto
  (commit c78a0ada). El arje-zero del núcleo (9967b02c) NO se toca ⇒ of_tree
  baseline (9adefb82/7fa6cb4e) BLINDADO. Separación Mecanismo/Política.
- scripts/attest-boot-test.sh: valida E2E sobre una copia del product-rootfs
  (override del PID1 por el gated + seed firmado por arje-packager + boot QEMU).

Validado in-VM (rootkey fija ⇒ firmas Ed25519 deterministas):
- ÍNTEGRO: gate atesta arje-zero+hammerd+busybox ✓ (politica=Halt) → servicios
  arriba → SSH OK.
- TAMPER (1 byte en hammerd tras firmar): "atestación ✗ binario no atestado" →
  "ARRANQUE FALLIDO ... Halt — abortando antes de incarnar" → shell de rescate,
  SIN SSH. La integridad comprometida NO levanta el entorno.

Pendiente (estructurar bien): cablear esto en hammer-bootstrap product() (firmar
+ hidratar el gated automáticamente) y reproducibilidad (Cargo.lock committeado
en tawasuyu para el gated). El mecanismo ya está probado.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 19:38:01 -04:00

93 lines
5.9 KiB
Bash
Executable File

#!/usr/bin/env bash
# attest-boot-test.sh — valida END-TO-END el gate de atestación (I4): hammer firma el seed (arje-packager)
# y el arje-zero CON gate (arje-zero-attest) lo verifica al arranque. Demuestra ambos caminos:
# - íntegro: el gate atesta los binarios críticos ✓ y arranca los servicios (SSH responde);
# - TAMPER=1: se altera 1 byte de un binario crítico DESPUÉS de firmar ⇒ el gate (política Halt)
# aborta y cae a la shell de rescate (sin SSH) — la integridad comprometida NO levanta el entorno.
#
# Es la contraparte de validación de la integración canónica firmada (capa de producto, baseline núcleo
# intacto). No ensambla el producto "de verdad" — overlaya el arje-zero con gate + el seed firmado sobre
# una copia del product-rootfs sellado, como spike, antes de integrarlo en hammer-bootstrap.
#
# Uso: PRODUCT=<hash> ./scripts/attest-boot-test.sh # caso íntegro (espera SSH OK)
# PRODUCT=<hash> TAMPER=1 ./scripts/attest-boot-test.sh # caso comprometido (espera HALT)
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"; cd "$ROOT"
KERNEL="${KERNEL:-$(ls store/*-linux/boot/bzImage 2>/dev/null | head -1)}"
MEM="${MEM:-2048}"; KVM="${KVM:-1}"; PORT="${PORT:-2226}"; DEADLINE="${DEADLINE:-130}"
WORK="$ROOT/work/attest-boot"; RFS="$WORK/rootfs"; LOG="$WORK/console.log"
pick(){ ls -dt store/*-"$1" 2>/dev/null | head -1; }
if [ -n "${PRODUCT:-}" ]; then P=$(ls -d store/"${PRODUCT#b3:}"*-product-rootfs 2>/dev/null | head -1); else P=$(pick product-rootfs); fi
GATED=$(pick arje-zero-attest); PKGR=$(pick arje-packager)
[ -n "$P" ] && [ -d "$P" ] || { echo "falta product-rootfs"; exit 1; }
[ -n "$GATED" ] && [ -e "$GATED/usr/bin/arje-zero" ] || { echo "falta arje-zero-attest (construí recipes/arje-zero-attest.toml)"; exit 1; }
[ -n "$PKGR" ] && [ -e "$PKGR/usr/bin/arje-packager" ] || { echo "falta arje-packager"; exit 1; }
echo "==> product $P"; echo "==> gated PID1 $GATED"; echo "==> packager $PKGR"
# copia escribible
rm -rf "$WORK"; mkdir -p "$WORK"; cp -a "$P"/. "$RFS"/; chmod -R u+w "$RFS"; rm -rf "$RFS/.hammer" "$RFS/ente/attest.json"
# override del PID1 por el arje-zero CON gate (romper hardlink)
rm -f "$RFS/usr/bin/arje-zero"; cp "$GATED/usr/bin/arje-zero" "$RFS/usr/bin/arje-zero"; chmod 0755 "$RFS/usr/bin/arje-zero"
# seed con attest_policy=halt como ENTRADA del firmador
python3 - "$RFS/ente/seed.card.json" "$WORK/seed-in.json" <<'PY'
import json,sys
d=json.load(open(sys.argv[1])); d["attest_policy"]="halt"
json.dump(d,open(sys.argv[2],"w"),indent=2)
PY
# rootkey fija (firmas Ed25519 deterministas ⇒ reproducible)
printf 'hammer-attest-dev-rootkey-0001!!' > "$WORK/rootkey.bin"
# FIRMAR: concesiones sobre el BLAKE3 de los binarios críticos REALES del staging
"$PKGR/usr/bin/arje-packager" --seed "$WORK/seed-in.json" --rootkey "$WORK/rootkey.bin" \
--bin arje-zero="$RFS/usr/bin/arje-zero" --bin hammerd="$RFS/usr/bin/hammerd" \
--bin console-getty="$RFS/bin/busybox" --bin sshd="$RFS/bin/busybox" \
--seed-out "$RFS/ente/seed.card.json" 2>&1 | sed 's/^/ /'
echo "==> seed firmado inyectado en /ente/seed.card.json (attest_policy=halt)"
# TAMPER: alterar 1 byte de un binario crítico DESPUÉS de firmar (su hash ya no casa la concesión)
if [ "${TAMPER:-0}" = 1 ]; then
B="$RFS/usr/bin/hammerd"; sz=$(stat -c%s "$B"); head -c "$sz" "$B" > "$B.n"; printf '\xde' | dd of="$B.n" bs=1 seek=64 count=1 conv=notrunc status=none; mv "$B.n" "$B"; chmod 0755 "$B"
echo "==> TAMPER: /usr/bin/hammerd alterado tras la firma — el gate debe HALT"
fi
# authorized_keys de prueba + empaquetar (root-owned)
KG=$(ls store/*-openssh/usr/bin/ssh-keygen|head -1); "$KG" -t ed25519 -N '' -f "$WORK/clientkey" -q
install -d -m 0700 "$RFS/root/.ssh"; cat "$WORK/clientkey.pub" > "$RFS/root/.ssh/authorized_keys"; chmod 0600 "$RFS/root/.ssh/authorized_keys"
( cd "$RFS" && find . -print0 | cpio --null -o -H newc -R 0:0 2>/dev/null | gzip -1 ) > "$WORK/initramfs.cpio.gz"
# boot
accel=(-cpu Broadwell); { [ "$KVM" = 1 ] && [ -w /dev/kvm ]; } && accel=(-enable-kvm -cpu host)
echo "==> boot QEMU (hostfwd :$PORT->:22)"
qemu-system-x86_64 -m "$MEM" -no-reboot -nographic "${accel[@]}" -kernel "$KERNEL" -initrd "$WORK/initramfs.cpio.gz" \
-append "console=ttyS0 rdinit=/sbin/init" -netdev "user,id=n0,hostfwd=tcp::$PORT-:22" -device e1000,netdev=n0 > "$LOG" 2>&1 &
QPID=$!; trap 'kill $QPID 2>/dev/null || true' EXIT
SSHOPTS=(-p "$PORT" -i "$WORK/clientkey" -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o GlobalKnownHostsFile=/dev/null -o PasswordAuthentication=no -o ConnectTimeout=4 -o LogLevel=ERROR)
echo "==> esperando (deadline ${DEADLINE}s)…"; start=$(date +%s); ssh_ok=0
while [ $(( $(date +%s) - start )) -lt "$DEADLINE" ]; do
kill -0 $QPID 2>/dev/null || break
if out=$(ssh "${SSHOPTS[@]}" root@localhost 'echo ATTEST_SSH_OK; uname -sr' 2>/dev/null); then echo "$out"|grep -q ATTEST_SSH_OK && ssh_ok=1 && break; fi
sleep 3
done
sleep 2; echo "---- consola: líneas de atestación ----"; grep -iE "atestaci|attest|ATESTACIÓN|integridad|rescate|ARRANQUE FALLIDO|✓|✗" "$LOG" | head -20
# veredicto
echo "========================================"
if [ "${TAMPER:-0}" = 1 ]; then
if [ "$ssh_ok" = 0 ] && grep -qiE "ATESTACIÓN FALLÓ|atestación al arranque falló|ARRANQUE FALLIDO|rescate" "$LOG"; then
echo "*** TAMPER OK — el gate detectó la manipulación y HALTeó (sin SSH) ***"; exit 0
else
echo "*** TAMPER FALLÓ — el sistema arrancó pese al binario alterado (ssh_ok=$ssh_ok) ***"; exit 1
fi
else
oks=$(grep -c "atestación ✓" "$LOG" 2>/dev/null || echo 0)
if [ "$ssh_ok" = 1 ] && [ "$oks" -ge 1 ] && ! grep -qiE "ATESTACIÓN FALLÓ|atestación al arranque falló" "$LOG"; then
echo "*** ATTEST OK — el gate atestó $oks binarios críticos ✓ y arrancó los servicios (SSH) ***"; exit 0
else
echo "*** ATTEST FALLÓ — íntegro pero no atestó/no SSH (ssh_ok=$ssh_ok, oks=$oks) ***"; exit 1
fi
fi