hammer-bootstrap: producto atestado cableado en product() (I4 / Etapa D, estructurar bien #1)

`product_attested()` + CLI `hammer bootstrap product --attest [--policy] [--rootkey]`:
estructura el spike attest-boot-test.sh dentro de hammer-bootstrap. Hidrata el init
CON gate (arje-zero-attest) sobre /usr/bin/arje-zero, fija attest_policy, deriva los
--bin label=path de la PROPIA seed (PID1 + execs Native, DFS) y FIRMA con arje-packager
(host, static musl) → seed firmada en /ente/seed.card.json; regenera /ente/attest.json
coherente con el init nuevo y sella product-attested-rootfs APARTE (núcleo+product base
intactos, Separación Mecanismo/Política).

- AttestConfig{policy, rootkey:[u8;32]}; DEV_ATTEST_ROOTKEY determinista ⇒ firmas
  Ed25519 reproducibles ⇒ árbol sellado reproducible. product_attested_hash v1.
- GOTCHA hardlinks read-only del store: romper /ente/seed.card.json antes de que el
  packager escriba (EACCES) y /ente/attest.json antes de reescribir; tmp de firma en
  staging/.attest-build se limpia antes de sellar.
- critical_bins_from_seed: getty+sshd comparten /bin/busybox ⇒ 3 concesiones por hash.
- Validado en host contra los sellos reales (3 concesiones, init 13MB, hammer attest ✓).
- Test hermético assemble_attested_swaps_gate_signs_seed_and_regenerates_attest (packager
  sintético). 40 tests verde.
- scripts/attest-boot-test.sh: MODO A REAL (PRODUCT_ATTESTED=<hash> bootea el artefacto
  real) + MODO B SPIKE fallback.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-20 20:01:41 -04:00
co-authored by Claude Opus 4.8
parent a44d05e7e2
commit 83d589a952
3 changed files with 446 additions and 37 deletions
+25 -7
View File
@@ -283,6 +283,17 @@ enum BootstrapCmd {
/// Directorio de recetas (`openssh.toml`, `netup.toml`, …).
#[arg(long, default_value = "recipes")]
recipes: String,
/// Produce además el producto ATESTADO (`product-attested-rootfs`): hidrata el init CON gate
/// (`arje-zero-attest`) y FIRMA la seed con `arje-packager`. Imprime el hash del atestado.
#[arg(long)]
attest: bool,
/// Política del gate de atestación: `halt` (default) | `degraded` | `warn`. Sólo con `--attest`.
#[arg(long, default_value = "halt")]
policy: String,
/// Fichero con la rootkey de 32 bytes raw para firmar. Sin él se usa la rootkey de desarrollo
/// determinista (firmas reproducibles). Sólo con `--attest`.
#[arg(long)]
rootkey: Option<String>,
},
/// [Stage 2] Ancla el content-hash del rootfs de Stage 1 como referencia de reproducibilidad
/// (anota la línea del manifiesto). Con `--verify <hash>` compara contra un rebuild `stage1'`
@@ -616,18 +627,25 @@ fn main() -> anyhow::Result<()> {
let hash = hammer_bootstrap::stage1(&spec, &base_cfg, &store)?;
println!("{hash}");
}
BootstrapCmd::Product { rootfs, recipes } => {
BootstrapCmd::Product { rootfs, recipes, attest, policy, rootkey } => {
let store = hammer_core::Store::open(&cli.store)?;
// El zig del lab (no la semilla): los servicios usan su `zig_version` propia
// (openssh ⇒ 0.13.0, hermano del default), y si ya están sellados, salen cacheados.
let base_cfg = hammer_build::BuildConfig::from_env_or_defaults(store.root());
let base = hammer_core::ArtifactHash::from_hex(rootfs.trim_start_matches("b3:"));
let hash = hammer_bootstrap::product(
&base,
&base_cfg,
std::path::Path::new(&recipes),
&store,
)?;
let recipes_dir = std::path::Path::new(&recipes);
let hash = if attest {
let mut cfg = hammer_bootstrap::AttestConfig { policy, ..Default::default() };
if let Some(path) = rootkey {
let bytes = std::fs::read(&path)?;
cfg.rootkey = bytes.as_slice().try_into().map_err(|_| {
anyhow::anyhow!("la rootkey {path} debe ser exactamente 32 bytes (son {})", bytes.len())
})?;
}
hammer_bootstrap::product_attested(&base, &base_cfg, recipes_dir, &store, &cfg)?
} else {
hammer_bootstrap::product(&base, &base_cfg, recipes_dir, &store)?
};
println!("{hash}");
}
BootstrapCmd::Stage2 { rootfs, verify } => {