250 líneas de comentario en 151 scripts. Control verificado: el diff no toca NI UNA línea que no empiece por #, y la sintaxis de los 151 pasa. El barrido saltea heredocs y cadenas triples, y el guardián DISPARÓ 3 veces: las tres eran el MOTD que el script escribe DENTRO de la imagen construida — texto del producto, no comentario del script. Se cambiaron aparte y a propósito, que es rebranding, no limpieza. Y el hallazgo caro: casaba contra , que es el TARGET de tracing — o sea el module_path!, o sea el nombre del crate. La etapa 4 lo movió a y el script quedó casando NADA. No fallaba: imprimía cero atribuciones, indistinguible de un log sin problemas. Comprobado con el binario (RUST_LOG=info sobre zlib), no deducido. Ahora acepta las dos, y tiene que seguir aceptándolas porque los logs viejos en disco dicen la vieja. Además 14 rutas de módulo en docs, que el barrido anterior no tocó porque no es frontera de palabra.
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): takana 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 `takana 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
|