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:
@@ -738,6 +738,265 @@ pub fn product(
|
||||
Ok(phash)
|
||||
}
|
||||
|
||||
// ── Producto ATESTADO: la capa FIRMADA que el gate de arje verifica al boot (I4 / Etapa D) ───────
|
||||
//
|
||||
// `product()` sella el `product-rootfs` con el arje-zero del núcleo + un `/ente/attest.json` de
|
||||
// AUTO-verificación (esquema paralelo que `hammer attest` valida, pero que el gate de arje NO lee).
|
||||
// El producto ATESTADO cierra el lazo canónico: hidrata el arje-zero CON gate (`arje-zero-attest`),
|
||||
// FIRMA la seed con `arje-packager` (una concesión Ed25519 por binario crítico, sobre su BLAKE3, bajo
|
||||
// una rootkey) y la sella como `product-attested-rootfs`. Booteado, el gate (política `halt`) recomputa
|
||||
// el BLAKE3 de cada exec crítico y aborta antes de incarnar si algo no casa la concesión firmada.
|
||||
//
|
||||
// Esto reemplaza el spike `scripts/attest-boot-test.sh` (que overlayeaba a mano): ahora hammer-bootstrap
|
||||
// produce el producto atestado de forma estructurada. Reusa la cripto canónica (arje-attest/agora vía el
|
||||
// packager) — cero criptografía nueva en hammer. La rootkey por defecto es determinista ⇒ las firmas
|
||||
// Ed25519 (RFC 8032, nonce derivado del mensaje) son reproducibles bit a bit.
|
||||
|
||||
/// Rootkey de desarrollo (32 bytes) con la que hammer firma la atestación por defecto. Determinista ⇒
|
||||
/// firmas reproducibles ⇒ `product-attested-rootfs` reproducible. El endurecimiento soberano (rootkey
|
||||
/// fuera del árbol, `/etc/arje/rootkey.pub` / `ARJE_ATTEST_ROOTKEY_FILE`) es una capa posterior
|
||||
/// (roadmap Etapa D, hardening #3); por ahora se inyecta como override de [`AttestConfig::rootkey`].
|
||||
pub const DEV_ATTEST_ROOTKEY: &[u8; 32] = b"hammer-attest-dev-rootkey-0001!!";
|
||||
|
||||
/// Recetas de la capa de atestación: el init CON gate (variante SÓLO producto, núcleo intacto) y el
|
||||
/// firmador (herramienta de build-time, estático musl, corre en el host y NO se embarca).
|
||||
const GATED_INIT_COMPONENT: &str = "arje-zero-attest";
|
||||
const PACKAGER_COMPONENT: &str = "arje-packager";
|
||||
|
||||
/// Configuración de la firma de atestación del producto.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AttestConfig {
|
||||
/// Política que el gate aplica si un binario no atesta: `"halt"` | `"degraded"` | `"warn"`.
|
||||
pub policy: String,
|
||||
/// Rootkey de 32 bytes para firmar las concesiones. Determinista ⇒ producto reproducible.
|
||||
pub rootkey: [u8; 32],
|
||||
}
|
||||
|
||||
impl Default for AttestConfig {
|
||||
/// Política `halt` (la integridad comprometida NO levanta el entorno) + la rootkey de desarrollo.
|
||||
fn default() -> Self {
|
||||
Self { policy: "halt".into(), rootkey: *DEV_ATTEST_ROOTKEY }
|
||||
}
|
||||
}
|
||||
|
||||
/// Identidad del `product-attested-rootfs`: el producto base + el init con gate + el firmador + la
|
||||
/// política + la rootkey. Cambiar cualquiera re-hashea; como las firmas Ed25519 son deterministas en
|
||||
/// estos insumos, el árbol sellado es reproducible.
|
||||
fn product_attested_hash(
|
||||
base_product: &RootfsHash,
|
||||
gated_init: &ArtifactHash,
|
||||
packager: &ArtifactHash,
|
||||
cfg: &AttestConfig,
|
||||
) -> RootfsHash {
|
||||
ArtifactHash::of_inputs(&[
|
||||
b"hammer-product-attested-v1",
|
||||
base_product.as_str().as_bytes(),
|
||||
gated_init.as_str().as_bytes(),
|
||||
packager.as_str().as_bytes(),
|
||||
cfg.policy.as_bytes(),
|
||||
&cfg.rootkey,
|
||||
])
|
||||
}
|
||||
|
||||
/// Binarios críticos que el gate atesta, derivados de la PROPIA seed (no una lista fija): el ejecutable
|
||||
/// de PID 1 (`arje-zero`, el `current_exe` del gate) + el `exec` de cada card `Native` del fractal
|
||||
/// `genesis` (recursivo), mapeado a su ruta dentro de `staging`. Así, añadir un servicio a la seed lo
|
||||
/// firma automáticamente. Devuelve `(label, ruta_host)` para alimentar `--bin label=path` del packager —
|
||||
/// el `label` debe casar el de la card (lo que el gate busca en las concesiones). El orden es estable
|
||||
/// (PID 1 primero, luego DFS del genesis) ⇒ los flags `--bin` son deterministas.
|
||||
fn critical_bins_from_seed(seed: &serde_json::Value, staging: &Path) -> Vec<(String, PathBuf)> {
|
||||
fn walk(card: &serde_json::Value, staging: &Path, out: &mut Vec<(String, PathBuf)>) {
|
||||
let exec = card
|
||||
.get("payload")
|
||||
.and_then(|p| p.get("Native"))
|
||||
.and_then(|n| n.get("exec"))
|
||||
.and_then(|e| e.as_str());
|
||||
if let (Some(exec), Some(label)) = (exec, card.get("label").and_then(|l| l.as_str())) {
|
||||
out.push((label.to_string(), staging.join(exec.trim_start_matches('/'))));
|
||||
}
|
||||
if let Some(g) = card.get("genesis").and_then(|g| g.as_array()) {
|
||||
for c in g {
|
||||
walk(c, staging, out);
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut out = vec![("arje-zero".to_string(), staging.join("usr/bin/arje-zero"))];
|
||||
if let Some(g) = seed.get("genesis").and_then(|g| g.as_array()) {
|
||||
for c in g {
|
||||
walk(c, staging, &mut out);
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// 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`).
|
||||
fn assemble_attested_product_rootfs(
|
||||
store: &Store,
|
||||
base_product: &RootfsHash,
|
||||
gated_init_bin: &Path,
|
||||
packager_bin: &Path,
|
||||
cfg: &AttestConfig,
|
||||
staging: &Path,
|
||||
) -> Result<()> {
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
|
||||
// 1) producto base hidratado (hardlinks read-only del árbol sellado).
|
||||
let base_dir = store.path_of(base_product, "product-rootfs");
|
||||
if !base_dir.is_dir() {
|
||||
return Err(Error::Other(format!(
|
||||
"product-rootfs base no sellado en {}",
|
||||
base_dir.display()
|
||||
)));
|
||||
}
|
||||
hammer_build::run_hydrate(&base_dir, staging, hammer_core::LinkMode::Static, None)?;
|
||||
let _ = std::fs::remove_dir_all(staging.join(".hammer"));
|
||||
|
||||
// 2) PISAR /usr/bin/arje-zero con el init CON gate. Es un hardlink read-only al store ⇒ romper el
|
||||
// hardlink (remove + copy), nunca `chmod` (mutaría el inodo del store).
|
||||
let init_dst = staging.join("usr/bin/arje-zero");
|
||||
let _ = std::fs::remove_file(&init_dst);
|
||||
std::fs::copy(gated_init_bin, &init_dst)?;
|
||||
std::fs::set_permissions(&init_dst, std::fs::Permissions::from_mode(0o755))?;
|
||||
|
||||
// 3) seed de producto: fijar `attest_policy` como ENTRADA del firmador. La mutamos y la dejamos en un
|
||||
// tmp FUERA del árbol a sellar (el seed-out lo escribe el packager sobre /ente/seed.card.json).
|
||||
let seed_path = staging.join("ente/seed.card.json");
|
||||
let raw = std::fs::read_to_string(&seed_path)
|
||||
.map_err(|e| Error::Other(format!("leer seed de producto: {e}")))?;
|
||||
let mut seed: serde_json::Value =
|
||||
serde_json::from_str(&raw).map_err(|e| Error::Other(format!("seed inválida: {e}")))?;
|
||||
seed["attest_policy"] = serde_json::Value::String(cfg.policy.clone());
|
||||
|
||||
let tmp = staging.join(".attest-build"); // se borra antes de sellar (no entra al árbol).
|
||||
std::fs::create_dir_all(&tmp)?;
|
||||
let seed_in = tmp.join("seed-in.json");
|
||||
std::fs::write(
|
||||
&seed_in,
|
||||
serde_json::to_string_pretty(&seed)
|
||||
.map_err(|e| Error::Other(format!("serializar seed-in: {e}")))?,
|
||||
)?;
|
||||
let rootkey_path = tmp.join("rootkey.bin");
|
||||
std::fs::write(&rootkey_path, cfg.rootkey)?;
|
||||
std::fs::set_permissions(&rootkey_path, std::fs::Permissions::from_mode(0o600))?;
|
||||
|
||||
// 4) bins críticos derivados de la seed → un --bin por exec Native + el PID 1.
|
||||
let bins = critical_bins_from_seed(&seed, staging);
|
||||
|
||||
// 5) FIRMAR: el packager escribe el seed firmado (Card::attest + attest_rootkey + attest_policy)
|
||||
// directamente sobre /ente/seed.card.json. El fichero hidratado es un HARDLINK read-only al
|
||||
// store ⇒ el packager no puede abrirlo para escribir (EACCES): romper el hardlink primero.
|
||||
let _ = std::fs::remove_file(&seed_path);
|
||||
let mut cmd = Command::new(packager_bin);
|
||||
cmd.arg("--seed")
|
||||
.arg(&seed_in)
|
||||
.arg("--rootkey")
|
||||
.arg(&rootkey_path)
|
||||
.arg("--seed-out")
|
||||
.arg(&seed_path);
|
||||
for (label, path) in &bins {
|
||||
cmd.arg("--bin").arg(format!("{label}={}", path.display()));
|
||||
}
|
||||
let out = cmd
|
||||
.output()
|
||||
.map_err(|e| Error::Other(format!("spawn arje-packager: {e}")))?;
|
||||
if !out.status.success() {
|
||||
return Err(Error::Other(format!(
|
||||
"arje-packager falló (exit {:?}): {}",
|
||||
out.status.code(),
|
||||
String::from_utf8_lossy(&out.stderr).trim()
|
||||
)));
|
||||
}
|
||||
|
||||
// 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.
|
||||
let attest = build_attestation(staging)?;
|
||||
let attest_json = serde_json::to_string_pretty(&attest)
|
||||
.map_err(|e| Error::Other(format!("serializar attest.json: {e}")))?;
|
||||
let attest_path = staging.join("ente/attest.json");
|
||||
let _ = std::fs::remove_file(&attest_path);
|
||||
std::fs::write(&attest_path, attest_json)?;
|
||||
|
||||
// 7) limpiar el tmp de build (no debe quedar en el árbol sellado).
|
||||
let _ = std::fs::remove_dir_all(&tmp);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// **Producto atestado** — toma el rootfs base verificado (`base`, el `stage1-rootfs` del 4/4), sella el
|
||||
/// `product-rootfs` (vía [`product`], idempotente) y produce el `product-attested-rootfs`: init CON gate
|
||||
/// + seed FIRMADA, listo para que el gate de arje lo verifique al boot (A2). Construye/cachea las recetas
|
||||
/// de la capa de atestación (`arje-zero-attest`, `arje-packager`). Idempotente. El núcleo y el producto
|
||||
/// base quedan intactos (Separación Mecanismo/Política): el atestado es un sello APARTE.
|
||||
pub fn product_attested(
|
||||
base: &RootfsHash,
|
||||
base_cfg: &hammer_build::BuildConfig,
|
||||
recipes_dir: &Path,
|
||||
store: &Store,
|
||||
cfg: &AttestConfig,
|
||||
) -> Result<RootfsHash> {
|
||||
// 0) el producto base (idempotente: si ya está sellado, sale cacheado).
|
||||
let base_product = product(base, base_cfg, recipes_dir, store)?;
|
||||
|
||||
// 1) recetas de la capa de atestación (idempotentes/cacheadas por el lab).
|
||||
let gated = build_components(&[GATED_INIT_COMPONENT], recipes_dir, base_cfg, store)?
|
||||
.pop()
|
||||
.map(|(_, h)| h)
|
||||
.ok_or_else(|| Error::Other("build de arje-zero-attest no devolvió hash".into()))?;
|
||||
let packager = build_components(&[PACKAGER_COMPONENT], recipes_dir, base_cfg, store)?
|
||||
.pop()
|
||||
.map(|(_, h)| h)
|
||||
.ok_or_else(|| Error::Other("build de arje-packager no devolvió hash".into()))?;
|
||||
|
||||
let gated_bin = store.path_of(&gated, GATED_INIT_COMPONENT).join("usr/bin/arje-zero");
|
||||
let packager_bin = store.path_of(&packager, PACKAGER_COMPONENT).join("usr/bin/arje-packager");
|
||||
if !gated_bin.is_file() {
|
||||
return Err(Error::Other(format!(
|
||||
"arje-zero (con gate) ausente en {}",
|
||||
gated_bin.display()
|
||||
)));
|
||||
}
|
||||
if !packager_bin.is_file() {
|
||||
return Err(Error::Other(format!(
|
||||
"arje-packager ausente en {}",
|
||||
packager_bin.display()
|
||||
)));
|
||||
}
|
||||
|
||||
// 2) identidad + ensamblado + sellado (si no estaba ya).
|
||||
let phash = product_attested_hash(&base_product, &gated, &packager, cfg);
|
||||
let store_name = "product-attested-rootfs";
|
||||
if !store.has(&phash, store_name) {
|
||||
let staging = store
|
||||
.root()
|
||||
.join(".bootstrap-tmp")
|
||||
.join(phash.store_dir_name(store_name));
|
||||
let _ = std::fs::remove_dir_all(&staging);
|
||||
std::fs::create_dir_all(&staging)?;
|
||||
let result = (|| -> Result<()> {
|
||||
assemble_attested_product_rootfs(
|
||||
store,
|
||||
&base_product,
|
||||
&gated_bin,
|
||||
&packager_bin,
|
||||
cfg,
|
||||
&staging,
|
||||
)?;
|
||||
store.seal(&staging, &phash, store_name)?;
|
||||
Ok(())
|
||||
})();
|
||||
let _ = std::fs::remove_dir_all(&staging);
|
||||
result?;
|
||||
tracing::info!(hash = %phash, "product: rootfs ATESTADO (init con gate + seed firmada) sellado");
|
||||
} else {
|
||||
tracing::info!(hash = %phash, "product: rootfs atestado ya sellado (idempotente)");
|
||||
}
|
||||
Ok(phash)
|
||||
}
|
||||
|
||||
// ── Stage 2: rebuild nativo + verificación de auto-alojamiento (SDD 11 §3) ──────────────────────
|
||||
//
|
||||
// El corte del cordón: dentro del rootfs de Stage 1 se reconstruyen Stage 0' y Stage 1' usando
|
||||
@@ -1820,6 +2079,134 @@ mod tests {
|
||||
assert!(rep3.missing.contains(&"/usr/sbin/sshd".to_string()) && !rep3.passed());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn assemble_attested_swaps_gate_signs_seed_and_regenerates_attest() {
|
||||
// Hermético: un `arje-packager` SINTÉTICO (script python) simula la firma — añade `attest`/
|
||||
// `attest_rootkey` al seed-out. Valida el CABLEADO de hammer (pisar el init, romper hardlinks,
|
||||
// fijar attest_policy, derivar los --bin de la seed, regenerar /ente/attest.json), sin la cripto
|
||||
// real (eso lo cubre E2E `scripts/attest-boot-test.sh` en QEMU).
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let store = Store::open(tmp.path().join("store")).unwrap();
|
||||
|
||||
// product-rootfs base sellado: seed de producto + /ente/attest.json + los bins críticos.
|
||||
let base = seal_component(&store, "product-rootfs", "9a07", |w| {
|
||||
std::fs::create_dir_all(w.join("usr/bin")).unwrap();
|
||||
std::fs::create_dir_all(w.join("bin")).unwrap();
|
||||
std::fs::create_dir_all(w.join("ente")).unwrap();
|
||||
std::fs::write(w.join("usr/bin/arje-zero"), b"\x7fELF-arje-zero-CORE").unwrap();
|
||||
std::fs::write(w.join("usr/bin/hammerd"), b"\x7fELF-hammerd").unwrap();
|
||||
std::fs::write(w.join("bin/busybox"), b"\x7fELF-busybox").unwrap();
|
||||
std::fs::write(w.join("ente/seed.card.json"), product_seed_card().unwrap()).unwrap();
|
||||
// attest.json del producto base (con el arje-zero del NÚCLEO) — debe regenerarse.
|
||||
let a = build_attestation(w).unwrap();
|
||||
std::fs::write(w.join("ente/attest.json"), serde_json::to_string(&a).unwrap()).unwrap();
|
||||
});
|
||||
|
||||
// init CON gate (bytes distintos al del núcleo) y packager sintético, fuera del store.
|
||||
let gated_bin = tmp.path().join("arje-zero-gated");
|
||||
std::fs::write(&gated_bin, b"\x7fELF-arje-zero-GATED-bigger").unwrap();
|
||||
let pkgr = tmp.path().join("fake-packager");
|
||||
std::fs::write(&pkgr, FAKE_PACKAGER).unwrap();
|
||||
std::fs::set_permissions(&pkgr, std::fs::Permissions::from_mode(0o755)).unwrap();
|
||||
|
||||
let staging = store.root().join(".bootstrap-tmp/test-attested");
|
||||
std::fs::create_dir_all(&staging).unwrap();
|
||||
let cfg = AttestConfig { policy: "halt".into(), rootkey: [3u8; 32] };
|
||||
assemble_attested_product_rootfs(&store, &base, &gated_bin, &pkgr, &cfg, &staging).unwrap();
|
||||
|
||||
// 1) /usr/bin/arje-zero ahora es el del gate (hardlink roto, no mutó el inodo del store).
|
||||
assert_eq!(
|
||||
std::fs::read(staging.join("usr/bin/arje-zero")).unwrap(),
|
||||
b"\x7fELF-arje-zero-GATED-bigger",
|
||||
"el init con gate pisó al del núcleo"
|
||||
);
|
||||
// El sello del store NO se tocó (su arje-zero sigue siendo el del núcleo).
|
||||
assert_eq!(
|
||||
std::fs::read(store.path_of(&base, "product-rootfs").join("usr/bin/arje-zero")).unwrap(),
|
||||
b"\x7fELF-arje-zero-CORE",
|
||||
"romper el hardlink no mutó el store"
|
||||
);
|
||||
// 2) seed firmada: el packager recibió attest_policy=halt y los 4 --bin derivados de la seed.
|
||||
let seed: serde_json::Value =
|
||||
serde_json::from_str(&std::fs::read_to_string(staging.join("ente/seed.card.json")).unwrap()).unwrap();
|
||||
assert_eq!(seed["attest_policy"], "halt");
|
||||
assert!(seed["attest"].is_array(), "el packager firmó (Card::attest)");
|
||||
let bins_seen = std::fs::read_to_string(staging.join("..").join("bins-seen.txt")).unwrap();
|
||||
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).
|
||||
assert!(verify_attestation(&staging).unwrap().passed(), "attest.json regenerado y coherente");
|
||||
// 4) el tmp de build se limpió.
|
||||
assert!(!staging.join(".attest-build").exists());
|
||||
}
|
||||
|
||||
/// `arje-packager` sintético para el test hermético: copia `--seed`→`--seed-out` inyectando
|
||||
/// `attest`/`attest_rootkey` (simula la firma) y vuelca los `--bin` vistos a `../bins-seen.txt`.
|
||||
const FAKE_PACKAGER: &str = r#"#!/usr/bin/env python3
|
||||
import json, sys
|
||||
seed=out=None; bins=[]
|
||||
a=sys.argv[1:]
|
||||
while a:
|
||||
f=a.pop(0)
|
||||
if f=="--seed": seed=a.pop(0)
|
||||
elif f=="--seed-out": out=a.pop(0)
|
||||
elif f=="--rootkey": a.pop(0)
|
||||
elif f=="--bin": bins.append(a.pop(0))
|
||||
else: pass
|
||||
d=json.load(open(seed))
|
||||
d["attest"]=[{"label":b.split("=",1)[0]} for b in bins]
|
||||
d["attest_rootkey"]="deadbeef"
|
||||
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))
|
||||
"#;
|
||||
|
||||
#[test]
|
||||
fn product_attested_hash_is_deterministic_and_sensitive() {
|
||||
let base = ArtifactHash::from_hex("ab");
|
||||
let gated = ArtifactHash::from_hex("cd");
|
||||
let pkgr = ArtifactHash::from_hex("ef");
|
||||
let cfg = AttestConfig::default();
|
||||
let a = product_attested_hash(&base, &gated, &pkgr, &cfg);
|
||||
assert_eq!(a, product_attested_hash(&base, &gated, &pkgr, &cfg), "determinista");
|
||||
// Cada insumo re-hashea.
|
||||
assert_ne!(a, product_attested_hash(&ArtifactHash::from_hex("11"), &gated, &pkgr, &cfg), "base");
|
||||
assert_ne!(a, product_attested_hash(&base, &ArtifactHash::from_hex("22"), &pkgr, &cfg), "gate");
|
||||
assert_ne!(a, product_attested_hash(&base, &gated, &ArtifactHash::from_hex("33"), &cfg), "packager");
|
||||
let other_policy = AttestConfig { policy: "warn".into(), ..AttestConfig::default() };
|
||||
assert_ne!(a, product_attested_hash(&base, &gated, &pkgr, &other_policy), "política");
|
||||
let other_key = AttestConfig { rootkey: [7u8; 32], ..AttestConfig::default() };
|
||||
assert_ne!(a, product_attested_hash(&base, &gated, &pkgr, &other_key), "rootkey");
|
||||
// El producto atestado es un sello DISTINTO del producto base (no colisionan).
|
||||
assert_ne!(a.as_str(), base.as_str(), "el atestado no es el producto base");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn critical_bins_are_derived_from_the_seed_native_execs() {
|
||||
// De la seed de producto real: PID 1 (arje-zero, el current_exe del gate) + un --bin por cada
|
||||
// exec Native del genesis (hammerd, console-getty, sshd). Los labels deben casar los de las cards
|
||||
// (lo que el gate busca en las concesiones firmadas) y las rutas resolver dentro del staging.
|
||||
let staging = Path::new("/tmp/fake-staging");
|
||||
let seed: serde_json::Value =
|
||||
serde_json::from_str(&product_seed_card().unwrap()).unwrap();
|
||||
let bins = critical_bins_from_seed(&seed, staging);
|
||||
let labels: Vec<&str> = bins.iter().map(|(l, _)| l.as_str()).collect();
|
||||
assert_eq!(
|
||||
labels,
|
||||
vec!["arje-zero", "hammerd", "console-getty", "sshd"],
|
||||
"PID 1 primero, luego DFS del genesis"
|
||||
);
|
||||
// El exec de cada card se mapea a su ruta absoluta dentro del staging.
|
||||
let by_label = |l: &str| bins.iter().find(|(x, _)| x == l).map(|(_, p)| p.clone()).unwrap();
|
||||
assert_eq!(by_label("arje-zero"), staging.join("usr/bin/arje-zero"));
|
||||
assert_eq!(by_label("hammerd"), staging.join("usr/bin/hammerd"));
|
||||
// console-getty y sshd ejecutan ambos /bin/busybox ⇒ misma ruta, labels distintos.
|
||||
assert_eq!(by_label("console-getty"), staging.join("bin/busybox"));
|
||||
assert_eq!(by_label("sshd"), staging.join("bin/busybox"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shipped_product_recipes_parse_and_hash() {
|
||||
// Las recetas de producto reales (userland Rust + servicios) cargan y hashean — viven fuera
|
||||
|
||||
Reference in New Issue
Block a user