diff --git a/recipes/arje-packager.toml b/recipes/arje-packager.toml new file mode 100644 index 00000000..194ec43d --- /dev/null +++ b/recipes/arje-packager.toml @@ -0,0 +1,25 @@ +# arje-packager — el firmador del seed de atestación (plan A1). HERRAMIENTA de build-time, NO se +# embarca en el producto: hammer lo construye y lo corre durante el ensamblado del product-rootfs para +# firmar las concesiones (`Card::attest`) sobre el BLAKE3 de los binarios críticos, bajo una rootkey. +# +# Con `--seed-out` (añadido en tawasuyu c78a0ada) emite el seed firmado STANDALONE (JSON), que hammer +# inyecta en /ente/seed.card.json del product-rootfs (que arma como disco, no como initramfs). El gate +# de arje-zero-attest lo verifica al boot (A2). Reusa la cripto canónica (arje-attest/agora) — cero +# criptografía nueva en hammer. +# +# Estático musl con zig cc: corre en el host (durante el ensamblado) sin loader, como busybox/bwrap. + +name = "arje-packager" +version = "0.0.1" + +[source] +repo = "gitea@git.tawasuyu.net:tawasuyu/tawasuyu.git" +commit = "c78a0adab1efc4aedddb25bcfa1f0abb341859d8" + +[build] +compiler = "zig-cc" +target = "x86_64-linux-musl" +link = "static" +# -p + --bin: el root del monorepo es un manifiesto VIRTUAL ⇒ `-p` elige el paquete, y como +# arje-packager tiene lib + bin, `cargo rustc -- ` exige además `--bin` (target único). +flags = ["-p", "arje-packager", "--bin", "arje-packager"] diff --git a/recipes/arje-zero-attest.toml b/recipes/arje-zero-attest.toml new file mode 100644 index 00000000..c9961876 --- /dev/null +++ b/recipes/arje-zero-attest.toml @@ -0,0 +1,24 @@ +# arje-zero-attest — variante de arje-zero CON el gate de atestación al arranque (A2), SÓLO para el +# PRODUCTO. No reemplaza al arje-zero del núcleo (recipes/arje-zero.toml, commit 9967b02c): ese sigue +# fijado para que el of_tree del self-host quede BLINDADO (baseline 9adefb82/7fa6cb4e). Esta variante se +# hidrata en la capa de producto, sobre /usr/bin/arje-zero — Separación Mecanismo/Política. +# +# El gate (init/arje-zero/src/attest_gate.rs, ya en tawasuyu main) computa el BLAKE3 de cada binario +# crítico del seed y lo contrasta con las concesiones firmadas en `Card::attest` (que hammer emite vía +# arje-packager). Política `Halt` ⇒ si algo no atesta, arje aborta el bucle y cae a la shell de rescate. +# +# Commit c78a0ada: tawasuyu main con el attest_gate + el `--seed-out` de arje-packager. (Pin a un commit +# con Cargo.lock committeado = hardening de reproducibilidad posterior; hoy construye funcional.) + +name = "arje-zero-attest" +version = "0.0.1" + +[source] +repo = "gitea@git.tawasuyu.net:tawasuyu/tawasuyu.git" +commit = "c78a0adab1efc4aedddb25bcfa1f0abb341859d8" + +[build] +compiler = "zig-cc" +target = "x86_64-linux-musl" +link = "static" +flags = ["-p", "arje-zero"] diff --git a/scripts/attest-boot-test.sh b/scripts/attest-boot-test.sh new file mode 100755 index 00000000..d0e18c42 --- /dev/null +++ b/scripts/attest-boot-test.sh @@ -0,0 +1,92 @@ +#!/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= ./scripts/attest-boot-test.sh # caso íntegro (espera SSH OK) +# PRODUCT= 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