🛡️ sandbox-labs GitHub ↗

05 · Custodia de claves y firma#

En una frase, para cualquiera: una clave de firma es como la llave de una caja fuerte: si alguien la copia, ya no sirve de nada cambiar la cerradura de golpe. Este caso hace que la llave entre en una habitación sin ventanas, firme allí dentro, y que no haya ningún camino por donde sacarla.

Estado real: 🟡 building · Carpeta: cases/05-smart-contracts/ · Puerto: 8805


Por qué se realiza este caso#

Casi todos los secretos se pueden rotar barato: cambias la contraseña y sigues. Una clave de firma no. Con ella se han emitido documentos, transacciones o paquetes que otros ya aceptaron como auténticos. Si se filtra, no solo hay que cambiarla: hay que revisar todo lo firmado desde que se filtró, y decidir qué sigue valiendo.

Lo que basta para perderla:

SituaciónCómo se pierde la clave
El proceso que firma tiene redUna línea de código la envía fuera
La clave está en una variable de entornoCualquier volcado de error la imprime
La clave está en un fichero del proyectoAcaba en el repositorio, y de ahí no se borra
El proceso que firma también hace otras cosasCualquier fallo de esas otras cosas la alcanza
Los registros incluyen el cuerpo de la peticiónLa clave queda escrita en texto plano

La idea que enseña, y que ningún otro caso enseña#

El secreto entra solo si tres cosas coinciden: el manifiesto de la carga declara que lo necesita, la política autoriza inyectarlo, y el entorno cumple lo que la política exige. Si falla cualquiera de las tres, no se ejecuta: falla cerrada y dice qué faltó.

Y dentro, no hay por dónde sacarla:

fuera del equipo.

ni en los registros.

ese fallo se comprueba en cada commit.

Todas las claves de este caso son de demostración, generadas en local y desechables. El proyecto no contiene, ni contendrá, claves de producción.

Casos de uso reales#

verificarlos.

el formato de evidencia.

Cómo funciona#

flowchart LR
  A["📄 Algo que firmar"] --> S["🧭 Servicio :8805"]
  S -- "socket Unix" --> J
  subgraph J["🔒 Jaula · red none · sin entorno"]
    K["🔑 Clave, inyectada solo si<br/>manifiesto + política + entorno coinciden"]
    F["✍️ Firma Ed25519"]
    K --> F
  end
  J --> R["🧾 Firma + acta"]
  J -. "intento de salida" .-> X["🚫 No hay red que usar"]

La comprobación de tres llaves#

flowchart TB
  A["Petición de firma"] --> B{"¿El manifiesto declara<br/>que necesita el secreto?"}
  B -- no --> N["🚫 No se ejecuta"]
  B -- sí --> C{"¿La política autoriza<br/>inyectar ese secreto?"}
  C -- no --> N
  C -- sí --> D{"¿El entorno cumple lo<br/>que la política exige?"}
  D -- no --> N
  D -- sí --> E["🔑 Se inyecta y se firma dentro"]
  N --> M["📣 Se explica qué faltó"]

Ese «no se ejecuta» es deliberado. La alternativa —ejecutar con menos controles de los pedidos— es la que produce sistemas que parecen seguros.

Esquemas#

Entrada — POST /api/sign#

{ "payload": { "to": "cuenta-b", "amount": 1250, "currency": "CLP" } }

Salida#

{
  "signature": "base64…",
  "algorithm": "ed25519",
  "publicKey": "base64…",
  "keyFingerprint": "sha256:…",
  "signedAt": "2026-08-07T03:00:00Z",
  "controls": { "network": "none", "transport": "unix-socket", "environment": "cleared" }
}

La clave privada no aparece en ningún campo de ninguna respuesta. Lo que sale es la firma, la clave pública y una huella para poder identificar qué clave se usó sin revelarla.

Comprobaciones auxiliares#

EndpointQué devuelve
GET /api/statusEn qué modo está: con clave cargada o solo planificando
GET /api/egressEl resultado de intentar salir a la red desde dentro: debe fallar

Software necesario#

ComponenteVersiónPara qué¿Obligatorio?
Rust1.75+ed25519-dalek para la firma, y sandboxctl
Python3.11+El servicio
bubblewrap0.6+La jaula sin red
Linux o WSL2kernel 5.10+Namespaces y sockets Unix

Instalación#

sudo apt install bubblewrap util-linux python3
cargo build --release
cargo run -p sandboxctl -- doctor

La clave de demostración se genera sola la primera vez, en

de versiones y debe seguir estándolo.**

Cómo se ejecuta#

cargo run -p sandboxctl -- service up smart-contracts

Y la verificación de que la evidencia firmada se sostiene:

cargo run -p sandboxctl -- evidence verify

Procesos que se crean#

sandboxctl service up smart-contracts
  │
  ├─ systemd --user scope
  │   └─ bwrap                   ← red none, entorno vacío, sin dispositivos
  │       └─ python3 app.py      ← firma, escuchando en socket Unix
  │
  └─ sandboxctl service forward  ← puente TCP :8805 ↔ socket Unix

Que el transporte sea un socket Unix no es un detalle de implementación: es el control. Un socket Unix vive en el sistema de ficheros, no en la red, así que no se alcanza desde otra máquina ni aunque el equipo esté expuesto.

Tiempo de carga#

OperaciónCoste típico
service up hasta que /health responde0,5–2 s
Generar la clave de demostración (una sola vez)< 10 ms
Una firma Ed25519< 1 ms
Verificar una firma< 1 ms
Prueba de exfiltración (/api/egress)< 50 ms, y debe fallar

Estado real y qué falta#

Construido: la firma Ed25519 dentro de la jaula, la red none, el transporte por socket Unix, la clave con permisos restringidos fuera del repositorio, y la firma de la evidencia del propio proyecto, encadenada y verificable con

Falta, y empieza por dividirlo en dos: este caso mezcla hoy dos ideas distintas. La custodia y la firma se quedan aquí, como

instrucciones, sin reloj, resultados reproducibles— es otra idea y se va al caso 07.

Falta también: límites de monto por firma, política de autorizaciones, rotación y revocación de claves.

Si algo falla#

SíntomaCausaCómo se soluciona
mode: plan en vez de liveNo hay clave cargada, así que el servicio muestra la petición que haría en vez de firmarEs el modo previsto sin secreto. Para firmar de verdad, dejar que se genere la clave de demostración en .sandbox-data/keys/ — se crea sola en el primer arranque
La firma no se ejecuta y dice que falta algoUna de las tres llaves no coincide: manifiesto, política o entornoEl mensaje dice cuál. Corregir el manifiesto de la carga o la política. No quitar el modo estricto para que pase: eso es exactamente lo que el caso enseña a no hacer
/api/egress no fallaEl sandbox tiene red cuando no debería1. Comprobar que la política dice network: none. 2. Ejecutar cargo run -p sandboxctl -- escape: si la sonda de red también sale, el problema es del entorno y no del caso
Permiso denegado al leer la claveTiene modo 0600 y pertenece a otro usuarioBorrarla y dejar que se regenere. No abrirla a más usuarios: una clave de firma legible por varios ya no prueba quién firmó
evidence verify dice que el hash de la política cambióEl código o la política cambiaron desde aquella ejecuciónNo es corrupción: es un acta vieja diciendo con razón que ya no describe el código de hoy. Volver a ejecutar para generar evidencia del estado actual
La clave privada aparece en un registro o en una respuestaFallo graveRotar la clave —borrarla y regenerarla— y abrir una incidencia. Nada del proyecto debe exponerla, y hay una prueba de exfiltración precisamente para detectar esto

Los fallos que afectan a cualquier caso —no se puede crear el sandbox, no hay cgroups, un puerto ocupado, procesos huérfanos, la compilación en Windows— están resueltos uno a uno en Cuando algo falla.


Ver también: Catálogo completo · Formato de evidencia · Estado del proyecto