El producto atestado escribe la pubkey de la rootkey en /etc/arje/rootkey.pub (32 bytes raw), leyéndola del attest_rootkey del seed ya firmado (lo que arje-packager computó de nuestra rootkey privada) ⇒ cero criptografía nueva en hammer. El gate de arje (attest_gate.rs::ancla_externa) la PREFIERE sobre la rootkey auto-declarada del seed (trust = ancla.or(seed.attest_rootkey)): cierra el ataque "seed reescrito por completo" que el WARN "sin ancla soberana externa" señalaba (hueco A2). product_attested_hash v2. - assemble_attested_product_rootfs: tras firmar, extrae attest_rootkey (array 32B) y lo escribe en etc/arje/rootkey.pub; test hermético verifica el ancla. - scripts/attest-boot-test.sh: nuevo modo SEED_REWRITE=1 (re-firma el seed con rootkey ATACANTE, deja la rootkey.pub legítima intacta ⇒ debe HALT). Validado E2E en QEMU sobre el artefacto REAL v2 (hammer bootstrap product --attest): · ÍNTEGRO → WARN desaparece, "anclada a rootkey soberana externa", 3 ✓, SSH OK. · SEED_REWRITE → gate: ancla ≠ attest_rootkey, 3× "autor no confiable" → Halt → sin SSH. Nota: el ancla en fichero cierra el seed-rewrite; el ancla compilada ARJE_ATTEST_ROOTKEY (dentro del binario atestado) sería más fuerte pero exige rebuild por-rootkey (documentada en attest_gate.rs, no hecha). Queda sólo el pendiente #2 (Cargo.lock en tawasuyu). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
119 lines
8.2 KiB
Bash
Executable File
119 lines
8.2 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.
|
|
#
|
|
# DOS MODOS:
|
|
# (A) REAL (preferido) — bootea el `product-attested-rootfs` que produce `hammer bootstrap product
|
|
# --attest` (init con gate + seed firmada ya hidratados). Es el camino de PRODUCCIÓN, no un spike.
|
|
# Uso: PRODUCT_ATTESTED=<hash> ./scripts/attest-boot-test.sh
|
|
# (B) SPIKE (fallback) — overlaya a mano el arje-zero con gate + firma el seed sobre una copia del
|
|
# `product-rootfs`. Útil cuando aún no se selló el atestado.
|
|
# Uso: PRODUCT=<hash> ./scripts/attest-boot-test.sh
|
|
# Con TAMPER=1 (en cualquier modo) se altera 1 byte de un binario crítico DESPUÉS de firmar ⇒ 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; }
|
|
rm -rf "$WORK"; mkdir -p "$WORK"
|
|
|
|
if [ -n "${PRODUCT_ATTESTED:-}" ] || { [ -z "${PRODUCT:-}" ] && PA=$(pick product-attested-rootfs) && [ -n "$PA" ]; }; then
|
|
# ── MODO A (REAL): el product-attested-rootfs ya trae el init con gate + el seed firmado ──────────
|
|
if [ -n "${PRODUCT_ATTESTED:-}" ]; then PA=$(ls -d store/"${PRODUCT_ATTESTED#b3:}"*-product-attested-rootfs 2>/dev/null | head -1); fi
|
|
[ -n "${PA:-}" ] && [ -d "$PA" ] || { echo "falta product-attested-rootfs (corré: hammer bootstrap product --rootfs <base> --attest)"; exit 1; }
|
|
echo "==> MODO REAL · product-attested $PA"
|
|
cp -a "$PA"/. "$RFS"/; chmod -R u+w "$RFS"; rm -rf "$RFS/.hammer"
|
|
echo "==> init con gate + seed firmada ya hidratados por hammer-bootstrap (sin overlay manual)"
|
|
else
|
|
# ── MODO B (SPIKE): overlay manual del gate + firma sobre el product-rootfs ───────────────────────
|
|
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 "==> MODO SPIKE · product $P · gated $GATED · packager $PKGR"
|
|
cp -a "$P"/. "$RFS"/; chmod -R u+w "$RFS"; rm -rf "$RFS/.hammer" "$RFS/ente/attest.json"
|
|
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"
|
|
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
|
|
printf 'hammer-attest-dev-rootkey-0001!!' > "$WORK/rootkey.bin" # rootkey fija ⇒ firmas deterministas
|
|
"$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)"
|
|
fi
|
|
|
|
# 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
|
|
|
|
# SEED_REWRITE: el ataque que el ANCLA SOBERANA (#3) cierra. Un atacante reescribe el seed firmando los
|
|
# binarios con SU rootkey y declara su pubkey en attest_rootkey (seed auto-consistente). Pero /etc/arje/
|
|
# rootkey.pub (la pubkey LEGÍTIMA, fuera de la Card) queda intacta ⇒ el gate exige firmas del ancla ⇒ las
|
|
# concesiones del atacante caen en AutorNoConfiable ⇒ Halt. Sin ancla, este seed arrancaría (hueco de A2).
|
|
if [ "${SEED_REWRITE:-0}" = 1 ]; then
|
|
PKGR=$(pick arje-packager)
|
|
[ -n "$PKGR" ] && [ -e "$PKGR/usr/bin/arje-packager" ] || { echo "falta arje-packager para SEED_REWRITE"; exit 1; }
|
|
[ -e "$RFS/etc/arje/rootkey.pub" ] || { echo "el product-attested-rootfs no trae /etc/arje/rootkey.pub (¿v2?)"; exit 1; }
|
|
python3 - "$RFS/ente/seed.card.json" "$WORK/rewrite-in.json" <<'PY'
|
|
import json,sys
|
|
d=json.load(open(sys.argv[1])); d.pop("attest",None); d.pop("attest_rootkey",None); d["attest_policy"]="halt"
|
|
json.dump(d,open(sys.argv[2],"w"),indent=2)
|
|
PY
|
|
printf 'hammer-EVIL-rootkey-attacker-001' > "$WORK/evil.bin" # 32 bytes, ≠ la rootkey legítima
|
|
"$PKGR/usr/bin/arje-packager" --seed "$WORK/rewrite-in.json" --rootkey "$WORK/evil.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" >/dev/null 2>&1
|
|
echo "==> SEED_REWRITE: seed re-firmado con rootkey ATACANTE; /etc/arje/rootkey.pub legítima intacta — 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 ] || [ "${SEED_REWRITE:-0}" = 1 ]; then
|
|
caso=$([ "${SEED_REWRITE:-0}" = 1 ] && echo "SEED_REWRITE" || echo "TAMPER")
|
|
if [ "$ssh_ok" = 0 ] && grep -qiE "ATESTACIÓN FALLÓ|atestación al arranque falló|ARRANQUE FALLIDO|rescate" "$LOG"; then
|
|
echo "*** $caso OK — el gate detectó el ataque y HALTeó (sin SSH) ***"; exit 0
|
|
else
|
|
echo "*** $caso FALLÓ — el sistema arrancó pese al ataque (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
|