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 --repo --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 --repo --trust --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=