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 1389Shell 137Markdown 80Rust 802602 Definiciones

repo: clave de release ESTABLE y un publicador por perfil — se acabó firmar con una clave efímera

`build-repo.sh` generaba una clave nueva en cada corrida si no se le pasaba `KEY=`. Un índice firmado
con una clave que nadie conoce y que cambia cada vez no lo verifica nadie: es decoración. El SDD 19
§3.2 lo dice sin vueltas — la firma sin gestión de claves es teatro.

- `trust/release.ed25519.pub` — la clave PÚBLICA, en el repo, que es donde tiene que estar para que
  un cliente pueda verificar. La privada vive en `~/.config/takana/keys/release.ed25519` (0600),
  fuera del repo, mismo trato que las credenciales del Storage Box.
- `trust/README.md` dice también **lo que esto NO es**: no hay clave raíz fuera de línea, ni
  rotación, ni procedimiento de filtración. Cierra el agujero de la clave efímera, NO el §3.2.
- `scripts/repo-perfil.sh` publica la clausura de un perfil y **aborta si no encuentra la clave**, en
  vez de inventar una. El conjunto sale de `yupana.membresia()` sobre `targets.toml`, la misma fuente
  que usa la imagen ⇒ el repo y la imagen no pueden divergir: son la misma lista.
- Y trae su propia guarda: tras firmar, VERIFICA el índice contra `trust/` y falla si no ancla.

Medido: 88/88 del perfil `servidor` con `expected_hash` anclado, índice firmado que verifica.
Control en los dos sentidos contra el repo servido por la caja:

    --trust ./trust        => "release: trusted (by release)"     ⇒ instala
    --trust <dir vacío>    => "release: unknown-key ... clave no confiada" ⇒ ABORTA

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RomoxEGZUhaT4pob1QSX5x

c0545ea40cbf530c9b80fc9a76a20551163574cd · por Sergio · (hace 1 mes) · padre 36be67fa9 · ver el árbol en este commit

Delta semántico

Ninguna definición cambió: el cambio es de texto, documentación o datos.

3 archivos · +129 −0

diff --git a/scripts/repo-perfil.sh b/scripts/repo-perfil.sh
new file mode 100755
index 00000000..514ee50c
--- /dev/null
+++ b/scripts/repo-perfil.sh
@@ -0,0 +1,83 @@
+#!/bin/sh
+# repo-perfil.sh — publica en un REPOSITORIO FIRMADO la clausura entera de un perfil de
+# `targets.toml`, y verifica la firma antes de darla por buena.
+#
+# ── EN QUÉ SE DIFERENCIA DE `build-repo.sh` ─────────────────────────────────────────────────────
+# `build-repo.sh` recorre las 869 recetas del corpus con `cargo run` por cada una: sirve para el
+# dogfood del corpus completo, no para publicar UNA imagen. Acá el conjunto sale de la misma fuente
+# que la imagen —`yupana.membresia()` sobre `targets.toml`— así que **el repo y la imagen no pueden
+# divergir**: son la misma lista.
+#
+# ── LA CLAVE NO SE INVENTA ──────────────────────────────────────────────────────────────────────
+# `build-repo.sh` genera una clave EFÍMERA si no se le pasa `KEY=`. Un índice firmado con una clave
+# que nadie conoce y que cambia en cada corrida no lo verifica nadie: es decoración (SDD 19 §3.2,
+# «la firma sin gestión de claves es teatro»). Acá, si no está la clave, se ABORTA.
+#
+# Uso:   ./scripts/repo-perfil.sh [perfil]
+# Env:   PERFIL (def servidor) · STORE (def ./store) · REPO (def dist/repo)
+#        KEY (def ~/.config/takana/keys/release.ed25519) · TRUST (def ./trust) · DISTRO (def dev)
+set -eu
+ROOT="$(cd "$(dirname "$0")/.." && pwd)"; cd "$ROOT"
+
+PERFIL="${1:-${PERFIL:-servidor}}"
+STORE="${STORE:-./store}"
+REPO="${REPO:-dist/repo}"
+KEY="${KEY:-$HOME/.config/takana/keys/release.ed25519}"
+TRUST="${TRUST:-./trust}"
+DISTRO="${DISTRO:-dev}"
+TAKANA="$ROOT/target/release/takana"
+
+[ -x "$TAKANA" ] || { echo "!! falta $TAKANA (cargo build --release)" >&2; exit 2; }
+[ -r "$KEY" ] || {
+    echo "!! no encuentro la clave de firma: $KEY" >&2
+    echo "   NO se genera una efímera a propósito: un índice firmado con una clave que nadie" >&2
+    echo "   conoce no lo verifica nadie. Generá la del proyecto una sola vez con:" >&2
+    echo "     takana keygen release --out ~/.config/takana/keys" >&2
+    echo "   y publicá la .pub en $TRUST/ (ver trust/README.md)." >&2
+    exit 2
+}
+
+# El conjunto sale de targets.toml, igual que la imagen.
+LISTA=$(python3 - "$PERFIL" <<'PY'
+import sys, os
+sys.path.insert(0, os.path.abspath("scripts"))
+import yupana, targets as targets_mod
+perfil = sys.argv[1]
+perfiles = targets_mod.load()
+if perfil not in perfiles:
+    sys.exit("perfil desconocido: %s" % perfil)
+memb = yupana.membresia(yupana.cargar_colas(), perfiles)
+for cola, nombre in sorted(n for n, ps in memb.items() if perfil in ps and n[0] != "wanted"):
+    if cola == "corpus":
+        print(nombre)
+PY
+)
+total=$(printf '%s\n' "$LISTA" | grep -c .)
+echo "==> perfil '$PERFIL': $total paquetes en la clausura → $REPO"
+
+rm -rf "$REPO"; mkdir -p "$REPO"
+anclados=0; sin_ancla=0; fallaron=""
+for n in $LISTA; do
+    r="recipes/$n.toml"
+    [ -r "$r" ] || { fallaron="$fallaron $n(sin-receta)"; continue; }
+    if "$TAKANA" --store "$STORE" pack "$r" --repo "$REPO" --distro-version "$DISTRO" --build >/dev/null 2>&1; then
+        anclados=$((anclados + 1))
+    elif "$TAKANA" --store "$STORE" pack "$r" --repo "$REPO" --distro-version "$DISTRO" >/dev/null 2>&1; then
+        sin_ancla=$((sin_ancla + 1))
+    else
+        fallaron="$fallaron $n"
+    fi
+done
+
+echo "==> firmando el índice con $KEY"
+"$TAKANA" --store "$STORE" repo sign --repo "$REPO" --key "$KEY" >/dev/null
+
+# ── la guarda: que la firma VERIFIQUE, y que una alterada NO ────────────────────────────────────
+"$TAKANA" --store "$STORE" repo verify --repo "$REPO" --trust "$TRUST" >/dev/null 2>&1 \
+    || { echo "!! el índice NO verifica contra $TRUST — la clave publicada no es la que firmó" >&2; exit 1; }
+echo "==> ✓ el índice verifica contra $TRUST"
+
+echo
+echo "publicado: $anclados con expected_hash anclado, $sin_ancla sin ancla.${fallaron:+ FALLARON:$fallaron}"
+echo "repo firmado en $REPO"
+echo "  consumir:  takana install <nombre> --repo <url> --trust <trust> --require-signed"
diff --git a/trust/README.md b/trust/README.md
new file mode 100644
index 00000000..d6947a9f
--- /dev/null
+++ b/trust/README.md
@@ -0,0 +1,45 @@
+# `trust/` — la raíz de confianza del repositorio de paquetes
+
+Acá vive **la clave PÚBLICA** con la que se firma el índice del repositorio. La privada **no está en
+git y no va a estarlo**: vive en `~/.config/takana/keys/release.ed25519`, modo 0600, fuera del repo —
+el mismo trato que las credenciales del Storage Box.
+
+## Por qué existe este directorio
+
+Hasta el 2026-09-10 `scripts/build-repo.sh` firmaba con una **clave efímera**: si no se le pasaba
+`KEY=`, generaba una nueva en cada corrida. Un índice firmado con una clave que nadie conoce y que
+cambia en cada build no lo verifica nadie — es decoración. El [SDD 19 §3.2](../docs/19-lanzamiento-publico.md)
+lo dice sin vueltas: *la firma sin gestión de claves es teatro*.
+
+Con la clave estable, un cliente puede hacer:
+
+```sh
+takana install <paquete> --repo <url> --trust <ruta a este dir> --require-signed
+```
+
+y eso **falla** si el índice no está firmado por esta clave. Que falle es el punto.
+
+## ⚠ Lo que esto NO es todavía
+
+Esto cierra el agujero de la clave efímera. **No cierra el SDD 19 §3.2**, que pide tres cosas más y
+ninguna existe:
+
+1. **Una clave raíz FUERA DE LÍNEA** que firme a las de release. Hoy hay un solo nivel: si esta clave
+   se filtra, no hay nada por encima que permita revocarla y emitir otra.
+2. **Rotación**: ningún procedimiento escrito de cómo se reemplaza, ni clientes que sepan aceptar más
+   de una clave durante la transición.
+3. **Qué hacer si se filtra**: sin lo anterior, la respuesta honesta hoy es «reinstalar la confianza a
+   mano en cada cliente», que no escala más allá de nuestras propias máquinas.
+
+Mientras la distribución sea privada (SDD 28 §2, «operativo pero privado») eso es aceptable. **Antes
+de la primera descarga pública, no.**
+
+## La clave vigente
+
+| | |
+|---|---|
+| fichero | `release.ed25519.pub` |
+| algoritmo | Ed25519 |
+| creada | 2026-09-10 |
+| pública (b64) | `FRrKg+eXlFuCHUzHNMCaN7eJAesFcgVOQI8KD06IXws=` |
+| privada | `~/.config/takana/keys/release.ed25519` (0600, **nunca a git**) |
diff --git a/trust/release.ed25519.pub b/trust/release.ed25519.pub
new file mode 100644
index 00000000..d91fb8a8
--- /dev/null
+++ b/trust/release.ed25519.pub
@@ -0,0 +1 @@
+FRrKg+eXlFuCHUzHNMCaN7eJAesFcgVOQI8KD06IXws=