gioser

sergio/takana

> Una distribución Linux construida con un laboratorio funcional y hermético en el > sótano, y una terminal mutable, clásica y anárquica en el piso de arriba — diseñada > desde el suelo para que una IA programadora entienda, modifique y co…

TOML 1574Shell 204Rust 111Markdown 993763 Definiciones

hammer-bootstrap: ancla soberana externa /etc/arje/rootkey.pub (I4 / Etapa D, hardening #3)

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>

4e7272730fedfd9e690083c6f897bccdb697157a · por sergio · (hace 4 meses) · padre 83d589a95 · ver el árbol en este commit

Delta semántico · 4

crates/hammer-bootstrap/src/lib.rs

2 archivos · +61 −10

diff --git a/crates/hammer-bootstrap/src/lib.rs b/crates/hammer-bootstrap/src/lib.rs
index 9b402bd2..f7c8e224 100644
--- a/crates/hammer-bootstrap/src/lib.rs
+++ b/crates/hammer-bootstrap/src/lib.rs
@@ -789,7 +789,7 @@ fn product_attested_hash(
     cfg: &AttestConfig,
 ) -> RootfsHash {
     ArtifactHash::of_inputs(&[
-        b"hammer-product-attested-v1",
+        b"hammer-product-attested-v2", // v2: + ancla soberana externa /etc/arje/rootkey.pub (hardening #3)
         base_product.as_str().as_bytes(),
         gated_init.as_str().as_bytes(),
         packager.as_str().as_bytes(),
@@ -831,9 +831,10 @@ fn critical_bins_from_seed(seed: &serde_json::Value, staging: &Path) -> Vec<(Str
 
 /// Ensambla el producto ATESTADO en `staging`: hidrata el `product-rootfs` base, PISA `/usr/bin/arje-zero`
 /// con el init CON gate, fija `attest_policy` en la seed, la FIRMA con el packager (una concesión por
-/// binario crítico sobre su BLAKE3) y regenera `/ente/attest.json` (la auto-verificación de hammer, ahora
-/// coherente con el arje-zero nuevo). El packager corre en el host (estático musl, sin loader, como
-/// busybox/bwrap). La firma se valida E2E en QEMU (`scripts/attest-boot-test.sh`).
+/// binario crítico sobre su BLAKE3), ancla la pubkey en `/etc/arje/rootkey.pub` (ancla soberana externa,
+/// hardening #3) y regenera `/ente/attest.json` (la auto-verificación de hammer, ahora coherente con el
+/// arje-zero nuevo). El packager corre en el host (estático musl, sin loader, como busybox/bwrap). La
+/// firma se valida E2E en QEMU (`scripts/attest-boot-test.sh`).
 fn assemble_attested_product_rootfs(
     store: &Store,
     base_product: &RootfsHash,
@@ -911,6 +912,30 @@ fn assemble_attested_product_rootfs(
         )));
     }
 
+    // 5b) ANCLA SOBERANA EXTERNA (hardening #3): escribir la pubkey de la rootkey en /etc/arje/rootkey.pub
+    //     (32 bytes raw). El gate (`ancla_externa()` en attest_gate.rs) la prefiere sobre la
+    //     `attest_rootkey` declarada en el seed ⇒ un seed reescrito con OTRA rootkey cae en AutorNoConfiable
+    //     (las concesiones no las firmó el ancla). Cierra el hueco que el WARN "sin ancla soberana externa"
+    //     señala. La pubkey se LEE del seed ya firmado (`attest_rootkey`, lo que el packager computó de
+    //     nuestra rootkey privada) ⇒ cero criptografía nueva en hammer.
+    let signed_raw = std::fs::read_to_string(&seed_path)
+        .map_err(|e| Error::Other(format!("releer seed firmado: {e}")))?;
+    let signed: serde_json::Value = serde_json::from_str(&signed_raw)
+        .map_err(|e| Error::Other(format!("seed firmado inválido: {e}")))?;
+    let pubkey: Vec<u8> = signed
+        .get("attest_rootkey")
+        .and_then(|v| v.as_array())
+        .map(|a| a.iter().filter_map(|x| x.as_u64().map(|n| n as u8)).collect::<Vec<u8>>())
+        .ok_or_else(|| Error::Other("seed firmado sin attest_rootkey de 32 bytes".into()))?;
+    if pubkey.len() != 32 {
+        return Err(Error::Other(format!(
+            "attest_rootkey del seed firmado no es de 32 bytes (es {})",
+            pubkey.len()
+        )));
+    }
+    std::fs::create_dir_all(staging.join("etc/arje"))?;
+    std::fs::write(staging.join("etc/arje/rootkey.pub"), &pubkey)?;
+
     // 6) regenerar /ente/attest.json: el arje-zero cambió ⇒ su BLAKE3 también, así que el manifiesto de
     //    auto-verificación de hammer (`hammer attest`) debe casar el árbol nuevo. El producto base lo
     //    trae hidratado (hardlink read-only): romperlo antes de reescribir.
@@ -2136,9 +2161,13 @@ mod tests {
         for label in ["arje-zero", "hammerd", "console-getty", "sshd"] {
             assert!(bins_seen.contains(&format!("{label}=")), "--bin {label} pasado al packager:\n{bins_seen}");
         }
-        // 3) /ente/attest.json regenerado: pasa con el arje-zero NUEVO (el del núcleo divergiría).
+        // 3) ANCLA SOBERANA EXTERNA: /etc/arje/rootkey.pub = la pubkey del seed firmado (32 bytes raw).
+        let anchor = std::fs::read(staging.join("etc/arje/rootkey.pub")).unwrap();
+        assert_eq!(anchor.len(), 32, "rootkey.pub son 32 bytes raw");
+        assert_eq!(anchor, vec![7u8; 32], "la pubkey del seed firmado anclada fuera de la Card");
+        // 4) /ente/attest.json regenerado: pasa con el arje-zero NUEVO (el del núcleo divergiría).
         assert!(verify_attestation(&staging).unwrap().passed(), "attest.json regenerado y coherente");
-        // 4) el tmp de build se limpió.
+        // 5) el tmp de build se limpió.
         assert!(!staging.join(".attest-build").exists());
     }
 
@@ -2157,7 +2186,7 @@ while a:
     else: pass
 d=json.load(open(seed))
 d["attest"]=[{"label":b.split("=",1)[0]} for b in bins]
-d["attest_rootkey"]="deadbeef"
+d["attest_rootkey"]=[7]*32  # pubkey de 32 bytes (como la del packager real)
 json.dump(d, open(out,"w"), indent=2)
 import os
 open(os.path.join(os.path.dirname(out),"..","..","bins-seen.txt"),"w").write("\n".join(bins))
diff --git a/scripts/attest-boot-test.sh b/scripts/attest-boot-test.sh
index 273227de..4aa38baf 100755
--- a/scripts/attest-boot-test.sh
+++ b/scripts/attest-boot-test.sh
@@ -58,6 +58,27 @@ if [ "${TAMPER:-0}" = 1 ]; then
   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"
@@ -80,11 +101,12 @@ sleep 2; echo "---- consola: líneas de atestación ----"; grep -iE "atestaci|at
 
 # veredicto
 echo "========================================"
-if [ "${TAMPER:-0}" = 1 ]; then
+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 "*** TAMPER OK — el gate detectó la manipulación y HALTeó (sin SSH) ***"; exit 0
+    echo "*** $caso OK — el gate detectó el ataque y HALTeó (sin SSH) ***"; exit 0
   else
-    echo "*** TAMPER FALLÓ — el sistema arrancó pese al binario alterado (ssh_ok=$ssh_ok) ***"; exit 1
+    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)