tawasuyu

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…

el llavero entre HERMANOS, medido: sí — pero sólo si el ancestro abrió sesión

El §7.terdecies cerró con esto dicho y «sin medir», y con la razón bien puesta:
`/proc/keys` como root no lo mide, sólo dice que la clave existe. Ver que `add_key`
funcionó no dice que el hermano pueda ENCONTRARLA, y en el LXC del worker esas dos cosas
se separan de la peor manera (add_key anda, KEYCTL_SEARCH da ENOSYS).

Medido en el anfitrión (kernel 7.1.2, root), en DOS escenarios porque el interesante es
el contraste — el primero solo, en rojo, se leería como «los hermanos no comparten
nunca», que es falso:

  A  el ancestro NO tiene sesión propia   sembrada ✓ · el hermano NO la encuentra
  B  el ancestro se une a una sesión      sembrada ✓ · el hermano la ENCUENTRA (mismo id)

O sea: `@s` sin sesión previa hace que el kernel le fabrique una AL PROCESO, y la seed
muere con quien la sembró. El flujo del escritorio —`agora-cli identity unlock` en una
terminal y `boveda` desde el lanzador— es posible, pero NO gratis: los dos tienen que
colgar de un ancestro con llavero de sesión. Si arje lanza cada app sin eso, la bóveda
contesta locked:true para siempre y no falla nada, que es la forma de fallo de todo el §7.

Tres controles, y los tres se dispararon de verdad:
· INSTRUMENTO — en el worker sale ENOSYS y la sonda se niega a contestar, reproduciendo
  sola la medición del §7.terdecies en vez de citarla;
· POSITIVO — el sembrador se busca su propia clave antes de salir;
· NEGATIVO — un tercer proceso salta a una sesión nueva y NO la encuentra. Sólo se corre
  donde el hermano SÍ encontró: si arriba salió «no», un «no» del control es el mismo
  «no» y no separa medir de no encontrar nunca.

En C y no en Python porque el python del anfitrión musl no trae `_ctypes` (falta
`libffi.so.8`) y se caía al importar, antes de medir nada; `keyutils` no está en el
corpus. Queda el syscall pelado, que además es lo que hace `pacha-llavero`.

40e9f4a12b5efc8007a2e0a3e8476cc5ac5cd1c8 · by Sergio · (1 month ago) · parent 45aadf890 · browse the tree at this commit

Semantic delta · 8

scripts/sonda-llavero-hermanos.c

2 files · +230 −0

diff --git a/scripts/sonda-llavero-hermanos.c b/scripts/sonda-llavero-hermanos.c
new file mode 100644
index 00000000..c24dfc77
--- /dev/null
+++ b/scripts/sonda-llavero-hermanos.c
@@ -0,0 +1,216 @@
+/* ¿Un proceso HERMANO ve la seed que otro sembró en el llavero de SESIÓN? (SDD 26 §7.terdecies)
+ *
+ *     scripts/sonda-llavero-hermanos.sh                      las dos etapas
+ *     scripts/sonda-llavero-hermanos.sh --negative-control   + el llavero de sesión NUEVO
+ *
+ * ── LA PREGUNTA, Y POR QUÉ NO ES ACADÉMICA ────────────────────────────────────────────────
+ * En las cuatro imágenes de escritorio la bóveda se abre así: la persona corre `agora-cli
+ * identity unlock`, que GUARDA la seed bajo `pacha_llavero::SEED_IDENTIDAD`; después abre
+ * `boveda` desde el lanzador, y esa app LEE la misma clave para descifrar la base. Son dos
+ * procesos distintos y ninguno es hijo del otro: HERMANOS, colgados los dos de la sesión.
+ *
+ * El §7.terdecies cerró dejando esto como pendiente y, textualmente, «sin medir — y /proc/keys
+ * como root no la mide, sólo dice que la clave existe». Es exacto: ver la clave en /proc/keys
+ * prueba que `add_key` funcionó, no que el hermano pueda ENCONTRARLA. Las dos preguntas se
+ * separaron de la peor manera en ese mismo capítulo: en el LXC del worker `add_key` funciona y
+ * `keyctl(KEYCTL_SEARCH)` da ENOSYS — ahí se puede sembrar y no se puede cosechar.
+ *
+ * Si los hermanos no compartieran llavero, el flujo del escritorio sería imposible POR
+ * CONSTRUCCIÓN —igual que el caso `servidor`— y la bóveda diría `locked:true` para siempre sin
+ * que nada falle.
+ *
+ * ── POR QUÉ EN C Y NO EN PYTHON ────────────────────────────────────────────────────────────
+ * El primer intento fue una sonda en Python con `ctypes`. No corre donde hace falta: el python
+ * del anfitrión musl no trae `_ctypes` (`libffi.so.8` no está) y se cae al importar, antes de
+ * medir nada. `keyutils`/`keyctl(1)` tampoco están en el corpus. Queda el syscall pelado, que es
+ * además lo que hace `pacha-llavero`.
+ *
+ * ── LOS TRES CONTROLES ─────────────────────────────────────────────────────────────────────
+ * 1. INSTRUMENTO: si `KEYCTL_SEARCH` da ENOSYS, un «no lo encontró» no prueba nada y se dice.
+ * 2. POSITIVO: el sembrador se busca su propia clave antes de salir. Si él no la ve, lo roto es
+ *    la sonda y no el llavero.
+ * 3. NEGATIVO: un tercer proceso hace `KEYCTL_JOIN_SESSION_KEYRING` —se va a una sesión nueva y
+ *    anónima— y busca lo mismo: TIENE que no encontrarlo. Sin esto, una sonda que encuentra
+ *    siempre se ve idéntica a una que mide.
+ *
+ * Los roles corren como `exec` APARTE, no como forks: un fork heredaría por definición y la
+ * sonda contestaría que sí sin haber medido nada. Lo que se reproduce es el escritorio: dos
+ * `exec` independientes de la misma sesión.
+ */
+#define _GNU_SOURCE
+#include <errno.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/syscall.h>
+#include <sys/wait.h>
+#include <unistd.h>
+
+#define KEY_SPEC_SESSION_KEYRING (-3)
+#define KEYCTL_JOIN_SESSION_KEYRING 1
+#define KEYCTL_SEARCH 10
+
+static long sembrar(const char *clave) {
+    static const char valor[] = "la seed de mentira";
+    return syscall(SYS_add_key, "user", clave, valor, sizeof valor - 1,
+                   KEY_SPEC_SESSION_KEYRING);
+}
+
+static long buscar(const char *clave) {
+    return syscall(SYS_keyctl, KEYCTL_SEARCH, KEY_SPEC_SESSION_KEYRING, "user", clave, 0L);
+}
+
+static long sesion_nueva(void) {
+    return syscall(SYS_keyctl, KEYCTL_JOIN_SESSION_KEYRING, (const char *)NULL);
+}
+
+static int rol_sembrador(const char *clave) {
+    long id = sembrar(clave);
+    if (id < 0) { printf("ADD_KEY_FALLO %s\n", strerror(errno)); return 1; }
+    long yo = buscar(clave);
+    if (yo >= 0) printf("SEMBRADA id=%ld yo_la_veo=si\n", id);
+    else         printf("SEMBRADA id=%ld yo_la_veo=NO(%s)\n", id, strerror(errno));
+    return 0;
+}
+
+static int rol_cosechador(const char *clave, int nueva) {
+    if (nueva) {
+        long s = sesion_nueva();
+        if (s < 0) { printf("JOIN_FALLO %s\n", strerror(errno)); return 1; }
+        printf("SESION_NUEVA id=%ld ", s);
+    }
+    long id = buscar(clave);
+    if (id >= 0) printf("ENCONTRADA id=%ld\n", id);
+    else         printf("NO_ENCONTRADA %s\n", strerror(errno));
+    return 0;
+}
+
+/* Lanza un rol como proceso APARTE y devuelve su primera línea en `salida`. */
+static int hermano(const char *yo, const char *rol, const char *clave, const char *extra,
+                   char *salida, size_t n) {
+    int tubo[2];
+    if (pipe(tubo) != 0) return -1;
+    pid_t p = fork();
+    if (p < 0) return -1;
+    if (p == 0) {
+        close(tubo[0]);
+        dup2(tubo[1], 1);
+        close(tubo[1]);
+        if (extra) execl(yo, yo, rol, clave, extra, (char *)NULL);
+        else       execl(yo, yo, rol, clave, (char *)NULL);
+        _exit(127);
+    }
+    close(tubo[1]);
+    ssize_t k = read(tubo[0], salida, n - 1);
+    salida[k > 0 ? k : 0] = 0;
+    char *nl = strchr(salida, '\n');
+    if (nl) *nl = 0;
+    close(tubo[0]);
+    int st = 0;
+    waitpid(p, &st, 0);
+    return 0;
+}
+
+static int escenario(const char *yo, const char *nombre, int con_sesion, int control) {
+    char clave[128], linea[512];
+    snprintf(clave, sizeof clave, "takana:sonda-hermanos:%d:%s", (int)getpid(),
+             con_sesion ? "con" : "sin");
+
+    printf("\n── ESCENARIO %s ──\n", nombre);
+    if (con_sesion) {
+        long s = sesion_nueva();
+        if (s < 0) {
+            printf("   ✗ no pude crear la sesión del ancestro (%s): este escenario no se midió\n",
+                   strerror(errno));
+            return 1;
+        }
+        printf("   el ancestro se une a un llavero de sesión propio (id=%ld) y RECIÉN AHÍ\n"
+               "   lanza a los dos hermanos, que lo heredan los dos.\n", s);
+    } else {
+        printf("   el ancestro NO tiene llavero de sesión propio: cada proceso que pide @s\n"
+               "   hace que el kernel le cree uno SUYO, al vuelo.\n");
+    }
+
+    if (hermano(yo, "sembrador", clave, NULL, linea, sizeof linea) != 0) {
+        fprintf(stderr, "no pude lanzar al sembrador\n");
+        return 1;
+    }
+    printf("   sembrador  : %s\n", linea);
+    if (strstr(linea, "ADD_KEY_FALLO")) {
+        printf("   ✗ no se pudo ni SEMBRAR: esta caja no tiene llavero de kernel usable. La\n"
+               "     pregunta queda SIN CONTESTAR acá — que no es contestada que no.\n");
+        return 1;
+    }
+    if (strstr(linea, "yo_la_veo=NO")) {
+        if (strstr(linea, "Function not implemented"))
+            printf("   ⚠ CONTROL DEL INSTRUMENTO EN ROJO: KEYCTL_SEARCH da ENOSYS acá (es el caso\n"
+                   "     medido del LXC del worker: se puede sembrar y no cosechar) ⇒ un «no lo\n"
+                   "     encontró» no probaría nada.\n");
+        else
+            printf("   ✗ CONTROL POSITIVO EN ROJO: el sembrador no ve su propia clave. Lo roto es\n"
+                   "     la sonda, no el llavero.\n");
+        return 1;
+    }
+
+    if (hermano(yo, "cosechador", clave, NULL, linea, sizeof linea) != 0) return 1;
+    printf("   hermano    : %s\n", linea);
+    int lo_ve = strstr(linea, "ENCONTRADA") && !strstr(linea, "NO_ENCONTRADA");
+
+    if (control && lo_ve) {
+        /* El control negativo SÓLO significa algo donde el hermano SÍ encontró: si arriba salió
+         * «no», un «no» del control es el mismo «no» y no separa medir de no encontrar nunca. */
+        char otra[512];
+        if (hermano(yo, "cosechador", clave, "--sesion-nueva", otra, sizeof otra) != 0) return 1;
+        printf("   control(-) : %s\n", otra);
+        if (strstr(otra, "JOIN_FALLO")) {
+            printf("   ⚠ el control negativo no se pudo montar: sin él, el «sí» de arriba no se\n"
+                   "     distingue de una sonda que encuentra siempre.\n");
+            return 1;
+        }
+        if (!strstr(otra, "NO_ENCONTRADA")) {
+            printf("   ✗ el control negativo la encontró IGUAL: la sonda no mide el llavero de\n"
+                   "     SESIÓN sino otra cosa (¿el de usuario?)\n");
+            return 1;
+        }
+        printf("   ✓ control negativo: en una sesión NUEVA la clave no está ⇒ esto mide el llavero\n");
+    }
+    return lo_ve ? 0 : 2;
+}
+
+int main(int argc, char **argv) {
+    if (argc >= 3 && !strcmp(argv[1], "sembrador"))  return rol_sembrador(argv[2]);
+    if (argc >= 3 && !strcmp(argv[1], "cosechador"))
+        return rol_cosechador(argv[2], argc > 3 && !strcmp(argv[3], "--sesion-nueva"));
+
+    int control = argc > 1 && !strcmp(argv[1], "--negative-control");
+    printf("── ¿un hermano ve la seed del llavero de SESIÓN? ──\n   uid : %d\n", (int)getuid());
+
+    /* Los dos escenarios se corren SIEMPRE y juntos, porque el interesante es el CONTRASTE: el
+     * primero solo, en rojo, se lee como «los hermanos no comparten nunca», que es falso. */
+    int sin_ses = escenario(argv[0], "A · el ancestro NO tiene sesión propia", 0, control);
+    int con_ses = escenario(argv[0], "B · el ancestro SÍ tiene sesión (lo que hace un login)", 1,
+                            control);
+
+    printf("\n── LO QUE SE MIDIÓ ──\n");
+    if (sin_ses == 0 && con_ses == 0) {
+        printf("✓ el hermano ve la seed en los DOS escenarios: la herencia no depende de que\n"
+               "  alguien abra sesión. El flujo del escritorio es posible.\n");
+        return 0;
+    }
+    if (con_ses == 0 && sin_ses == 2) {
+        printf("✓ el hermano ve la seed SÓLO si el ancestro común abrió sesión (B), y no la ve si\n"
+               "  cada proceso se fabrica la suya al vuelo (A).\n"
+               "⇒ el flujo del escritorio es posible, pero NO gratis: `agora-cli unlock` y `boveda`\n"
+               "  tienen que colgar de un ancestro con llavero de SESIÓN. Si arje lanza cada app\n"
+               "  sin eso, la seed muere con el proceso que la sembró y la bóveda contesta\n"
+               "  locked:true para siempre, sin un solo error — que es la forma de fallo del §7.\n");
+        return 0;
+    }
+    if (con_ses == 2) {
+        printf("✗ ni con sesión propia el hermano ve la seed: el flujo del escritorio sería\n"
+               "  imposible por construcción, igual que el caso `servidor` del §7.terdecies.\n");
+        return 1;
+    }
+    printf("⚠ alguno de los dos escenarios no se pudo medir acá (ver arriba): sin contestar.\n");
+    return 1;
+}
diff --git a/scripts/sonda-llavero-hermanos.sh b/scripts/sonda-llavero-hermanos.sh
new file mode 100755
index 00000000..b5f956b7
--- /dev/null
+++ b/scripts/sonda-llavero-hermanos.sh
@@ -0,0 +1,14 @@
+#!/bin/sh
+# Compila y corre `sonda-llavero-hermanos.c`. Ver el cabezal del .c para qué mide y por qué.
+#
+#     scripts/sonda-llavero-hermanos.sh
+#     scripts/sonda-llavero-hermanos.sh --negative-control
+#
+# El binario va a un temporal a propósito: es un INSTRUMENTO, no entra en ninguna imagen ni en el
+# store. `CC` se puede pisar (`CC=gcc scripts/sonda-llavero-hermanos.sh`).
+set -e
+AQUI=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
+BIN=${TMPDIR:-/tmp}/sonda-llavero-hermanos.$$
+trap 'rm -f "$BIN"' EXIT INT TERM
+"${CC:-cc}" -O1 -o "$BIN" "$AQUI/sonda-llavero-hermanos.c"
+"$BIN" "$@"