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 1576Shell 205Rust 111Markdown 993774 Definiciones

⎇ 2c2ee951d6d32b49c88c5673f483168e8b2cce43 scripts/harkaq/harkaq-audit.c C · 301 líneas · 13.3 KB Historia Quién tocó qué crudo

cierres §3: política de runtime derivada por medición (sin tocar la cripto) be52e735a · sergio ·

1// harkaq-audit — el lector de evidencia (SDD 16 §4). Vive en el HOST, junto a hammerd, fuera
2// de bwrap: el audit no está namespaceado y los registros del build cruzan (medido, §3.5).
3//
4// Contrato: se lanza ANTES del build, con el canario de ese build. Espera el registro del
5// canario (que revela el `domain=`), junta las demás denegaciones de ESE dominio, y al
6// liberarse el dominio compara su conteo contra el del kernel. Emite el Verdict en JSON.
7//
8//   harkaq-audit --canary /harkaq-canary-<nonce> [--timeout 600] [--ready-fd N]
9//
10// Los tres estados (D9) — `denials=[]` NO se cree, se gana:
11//   Hermetico    canario visto + cero denegaciones + conteo coherente
12//   Impuro       canario visto + N denegaciones (el diagnóstico útil)
13//   SinEvidencia canario NO visto, o el conteo del kernel no cuadra ⇒ no se afirma NADA
14//
15// Requiere CAP_AUDIT_READ (multicast) y, si audit_enabled=0, CAP_AUDIT_CONTROL. Para el bucle
16// de desarrollo:  sudo setcap cap_audit_read,cap_audit_control+ep harkaq-audit
17
18#define _GNU_SOURCE
19#include "harkaq-uapi.h"
20#include <linux/netlink.h>
21#include <signal.h>
22#include <stdio.h>
23#include <stdlib.h>
24#include <string.h>
25#include <sys/socket.h>
26#include <time.h>
27#include <unistd.h>
28
29// SIGTERM ⇒ emitir el veredicto y salir, en vez de morir mudo. El camino normal es salir solo
30// al ver el `deallocated` del dominio (que siempre llega: el canario garantiza que el dominio
31// loguea, y un dominio que loguea sí emite su dealloc — §3.4). Esto cubre el otro caso: que el
32// build ni siquiera arranque, donde sin señal el lector esperaría el timeout entero.
33static volatile sig_atomic_t parar = 0;
34static void on_term(int s) { (void)s; parar = 1; }
35
36#define MAX_DENIALS 256
37
38struct denial {
39    char blockers[128];
40    char path[1024];
41    char dev[64];
42    char ino[32];
43};
44
45// ---------------------------------------------------------------- parseo de registros
46
47// Extrae `key=valor` del texto del registro. Quita comillas si las hay. El kernel emite
48// p.ej.: domain=146973ef2 blockers=fs.read_file path="/etc/passwd" dev="nvme1n1p6" ino=4458369
49static int field(const char *rec, const char *key, char *out, size_t cap) {
50    char pat[64];
51    snprintf(pat, sizeof(pat), " %s=", key);
52    const char *p = strstr(rec, pat);
53    if (!p) {  // puede estar al principio, sin espacio delante
54        snprintf(pat, sizeof(pat), "%s=", key);
55        if (strncmp(rec, pat, strlen(pat)) != 0) return 0;
56        p = rec;
57    } else {
58        p++;
59    }
60    p += strlen(key) + 1;
61    int quoted = (*p == '"');
62    if (quoted) p++;
63    size_t i = 0;
64    while (*p && i + 1 < cap) {
65        if (quoted ? (*p == '"') : (*p == ' ')) break;
66        out[i++] = *p++;
67    }
68    out[i] = 0;
69    return 1;
70}
71
72// Escapa una cadena para meterla en JSON sin romperlo (los paths pueden traer comillas).
73static void json_str(const char *s, char *out, size_t cap) {
74    size_t i = 0;
75    for (; *s && i + 2 < cap; s++) {
76        if (*s == '"' || *s == '\\') {
77            out[i++] = '\\';
78            out[i++] = *s;
79        } else if ((unsigned char)*s < 0x20) {
80            i += snprintf(out + i, cap - i, "\\u%04x", *s);
81        } else {
82            out[i++] = *s;
83        }
84    }
85    out[i] = 0;
86}
87
88// ---------------------------------------------------------------- netlink
89
90static int nl_send(int fd, int type, const void *payload, size_t len) {
91    struct { struct nlmsghdr h; char data[256]; } req = {0};
92    req.h.nlmsg_len = NLMSG_LENGTH(len);
93    req.h.nlmsg_type = type;
94    req.h.nlmsg_flags = NLM_F_REQUEST;  // sin ACK: la respuesta de AUDIT_GET es asíncrona (§3.4)
95    req.h.nlmsg_seq = 1;
96    req.h.nlmsg_pid = getpid();
97    if (payload) memcpy(req.data, payload, len);
98    struct sockaddr_nl sa = {.nl_family = AF_NETLINK};
99    return sendto(fd, &req, req.h.nlmsg_len, 0, (struct sockaddr *)&sa, sizeof(sa));
100}
101
102static int audit_status(int *enabled) {
103    int fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_AUDIT);
104    if (fd < 0) return -1;
105    if (nl_send(fd, AUDIT_GET, NULL, 0) < 0) { close(fd); return -1; }
106    struct timeval tv = {.tv_sec = 1};
107    setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
108    char buf[8192];
109    for (int i = 0; i < 8; i++) {
110        ssize_t n = recv(fd, buf, sizeof(buf), 0);
111        if (n <= 0) break;
112        for (struct nlmsghdr *h = (struct nlmsghdr *)buf; NLMSG_OK(h, n); h = NLMSG_NEXT(h, n))
113            if (h->nlmsg_type == AUDIT_GET) {
114                *enabled = ((struct audit_status *)NLMSG_DATA(h))->enabled;
115                close(fd);
116                return 0;
117            }
118    }
119    close(fd);
120    return -1;
121}
122
123static int audit_enable(void) {
124    int fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_AUDIT);
125    if (fd < 0) return -1;
126    struct audit_status st = {.mask = AUDIT_STATUS_ENABLED, .enabled = 1};
127    int r = nl_send(fd, AUDIT_SET, &st, sizeof(st));
128    close(fd);
129    return r < 0 ? -1 : 0;
130}
131
132// ---------------------------------------------------------------- veredicto
133
134static void emit(const char *estado, int canario, const char *domain, struct denial *d, int nd,
135                 int kernel_count, const char *motivo) {
136    char esc[2048];
137    printf("{\"estado\":\"%s\",\"canario_visto\":%s,\"domain\":\"%s\",\"contador_kernel\":%d,",
138           estado, canario ? "true" : "false", domain, kernel_count);
139    if (motivo) {
140        json_str(motivo, esc, sizeof(esc));
141        printf("\"motivo\":\"%s\",", esc);
142    }
143    printf("\"denials\":[");
144    for (int i = 0; i < nd; i++) {
145        json_str(d[i].path, esc, sizeof(esc));
146        printf("%s{\"blockers\":\"%s\",\"path\":\"%s\",\"dev\":\"%s\",\"ino\":\"%s\"}",
147               i ? "," : "", d[i].blockers, esc, d[i].dev, d[i].ino);
148    }
149    printf("]}\n");
150    fflush(stdout);
151}
152
153int main(int argc, char **argv) {
154    const char *canary = NULL;
155    int timeout = 600, ready_fd = -1;
156    for (int i = 1; i < argc; i++) {
157        if (!strcmp(argv[i], "--canary") && i + 1 < argc) canary = argv[++i];
158        else if (!strcmp(argv[i], "--timeout") && i + 1 < argc) timeout = atoi(argv[++i]);
159        else if (!strcmp(argv[i], "--ready-fd") && i + 1 < argc) ready_fd = atoi(argv[++i]);
160    }
161    if (!canary) {
162        fprintf(stderr, "uso: harkaq-audit --canary <path> [--timeout s] [--ready-fd n]\n");
163        return 2;
164    }
165
166    // GATE: sin audit_enabled=1 el kernel no emite NADA y un `denials=[]` sería el instrumento
167    // callado, no un build limpio (§3.4). Nunca reportar; abortar.
168    int enabled = -1;
169    audit_status(&enabled);
170    if (enabled != 1) {
171        audit_enable();
172        usleep(100000);
173        audit_status(&enabled);
174    }
175    if (enabled != 1) {
176        emit("SinEvidencia", 0, "", NULL, 0, -1,
177             "audit_enabled != 1: el kernel no emite registros (¿CAP_AUDIT_CONTROL? ¿audit=1?)");
178        return 4;
179    }
180
181    int nl = socket(AF_NETLINK, SOCK_RAW, NETLINK_AUDIT);
182    struct sockaddr_nl sa = {.nl_family = AF_NETLINK, .nl_groups = 1 << (AUDIT_NLGRP_READLOG - 1)};
183    if (nl < 0 || bind(nl, (struct sockaddr *)&sa, sizeof(sa)) < 0) {
184        emit("SinEvidencia", 0, "", NULL, 0, -1,
185             "no pude abrir el multicast del audit (¿falta CAP_AUDIT_READ?)");
186        return 4;
187    }
188    // Sólo ahora el build puede arrancar: si arrancara antes, sus primeras denegaciones —el
189    // canario entre ellas— se emitirían sin nadie escuchando y el veredicto sería SinEvidencia
190    // por una carrera, no por un problema real.
191    if (ready_fd >= 0) { (void)!write(ready_fd, "R", 1); close(ready_fd); }
192
193    struct denial denials[MAX_DENIALS];
194    int nd = 0, canary_seen = 0, kernel_count = -1;
195    int drenando = 0;   // el dominio ya murió: drenar lo que quede antes de emitir
196    // ACCESS que llegan ANTES de ver el canario: no se pueden filtrar todavía (el `domain=` del
197    // build se conoce al ver el canario), pero pueden ser SUYOS — el `sh` deniega cosas antes de
198    // llegar al probe. Descartarlos perdía registros y el contador del kernel lo cazaba (4 vs 1).
199    // Se bufferizan con su domain y se filtran retroactivamente.
200    static struct { char dom[64]; struct denial d; } previos[MAX_DENIALS];
201    int n_prev = 0;
202    char domain[64] = "";
203    struct timeval tv = {.tv_sec = 1};
204    setsockopt(nl, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
205    char buf[16384];
206    time_t deadline = time(NULL) + timeout;
207    struct sigaction act = {.sa_handler = on_term};
208    sigaction(SIGTERM, &act, NULL);
209    sigaction(SIGINT, &act, NULL);
210
211    while (time(NULL) < deadline && !parar) {
212        ssize_t n = recv(nl, buf, sizeof(buf), 0);
213        // Drenando: el dominio murió y sólo faltan los ACCESS rezagados. Si el socket se agota
214        // (timeout de 1s) o ya cuadramos con el contador del kernel, cerramos.
215        if (drenando && (n <= 0 || (kernel_count >= 0 && nd + 1 >= kernel_count))) goto done;
216        if (n <= 0) continue;
217        for (struct nlmsghdr *h = (struct nlmsghdr *)buf; NLMSG_OK(h, n); h = NLMSG_NEXT(h, n)) {
218            if (h->nlmsg_type != AUDIT_LANDLOCK_ACCESS && h->nlmsg_type != AUDIT_LANDLOCK_DOMAIN)
219                continue;
220            char rec[8192];
221            size_t len = NLMSG_PAYLOAD(h, 0);
222            if (len >= sizeof(rec)) len = sizeof(rec) - 1;
223            memcpy(rec, NLMSG_DATA(h), len);
224            rec[len] = 0;
225
226            char dom[64] = "";
227            field(rec, "domain", dom, sizeof(dom));
228
229            if (h->nlmsg_type == AUDIT_LANDLOCK_ACCESS) {
230                char path[1024] = "";
231                field(rec, "path", path, sizeof(path));
232                // El canario revela NUESTRO dominio. Es la clave primaria: `pid=` viaja en el
233                // `allocated`, que es perezoso, y dev+ino identifican el objeto y no el build
234                // (dos builds leyendo el mismo fichero dan el mismo ino) — §3.5.
235                if (!canary_seen && !strcmp(path, canary)) {
236                    canary_seen = 1;
237                    snprintf(domain, sizeof(domain), "%s", dom);
238                    // Rescatar del buffer los ACCESS previos que resultaron ser de MI dominio.
239                    for (int k = 0; k < n_prev && nd < MAX_DENIALS; k++)
240                        if (!strcmp(previos[k].dom, domain)) denials[nd++] = previos[k].d;
241                    continue;  // el canario no es una denegación del build: es el sello del lector
242                }
243                if (!canary_seen) {
244                    // Aún no sé mi domain: guardar por si resulta ser mío.
245                    if (n_prev < MAX_DENIALS) {
246                        snprintf(previos[n_prev].dom, sizeof(previos[n_prev].dom), "%s", dom);
247                        field(rec, "blockers", previos[n_prev].d.blockers, sizeof(previos[n_prev].d.blockers));
248                        snprintf(previos[n_prev].d.path, sizeof(previos[n_prev].d.path), "%s", path);
249                        field(rec, "dev", previos[n_prev].d.dev, sizeof(previos[n_prev].d.dev));
250                        field(rec, "ino", previos[n_prev].d.ino, sizeof(previos[n_prev].d.ino));
251                        n_prev++;
252                    }
253                    continue;
254                }
255                if (strcmp(dom, domain) != 0) continue;  // de otro build
256                if (nd < MAX_DENIALS) {
257                    field(rec, "blockers", denials[nd].blockers, sizeof(denials[nd].blockers));
258                    snprintf(denials[nd].path, sizeof(denials[nd].path), "%s", path);
259                    field(rec, "dev", denials[nd].dev, sizeof(denials[nd].dev));
260                    field(rec, "ino", denials[nd].ino, sizeof(denials[nd].ino));
261                    nd++;
262                }
263            } else if (canary_seen && !strcmp(dom, domain)) {
264                char status[32] = "";
265                field(rec, "status", status, sizeof(status));
266                if (!strcmp(status, "deallocated")) {
267                    char cnt[32] = "";
268                    if (field(rec, "denials", cnt, sizeof(cnt))) kernel_count = atoi(cnt);
269                    // NO salir de una: el `deallocated` puede adelantarse a registros ACCESS que
270                    // siguen en el buffer del socket. Con un inquilino instantáneo (`htop
271                    // --version`) el kernel contaba 4 denegaciones y el lector recibía 1 ⇒ el
272                    // chequeo anti-pérdida disparaba SinEvidencia. Se drena hasta cuadrar con el
273                    // contador (o agotar el socket), y recién ahí se cierra.
274                    drenando = 1;
275                    continue;
276                }
277            }
278        }
279    }
280
281done:
282    if (!canary_seen) {
283        // La afirmación central de harkaq (D2) sería indistinguible de un lector ciego.
284        emit("SinEvidencia", 0, domain, denials, nd, kernel_count,
285             "canario NO visto: el logging post-exec no llegó (¿falta LOG_NEW_EXEC_ON?) o el "
286             "lector no es confiable. Un build hermético y un lector ciego son bit-idénticos.");
287        return 3;
288    }
289    // El contador del kernel caza registros PERDIDOS por desborde del backlog de audit — un
290    // riesgo real con la granja construyendo en paralelo (§3.4). +1 = el propio canario.
291    if (kernel_count >= 0 && kernel_count != nd + 1) {
292        char m[256];
293        snprintf(m, sizeof(m),
294                 "el kernel contó %d denegaciones y recibí %d (+canario): se perdieron registros",
295                 kernel_count, nd + 1);
296        emit("SinEvidencia", 1, domain, denials, nd, kernel_count, m);
297        return 3;
298    }
299    emit(nd == 0 ? "Hermetico" : "Impuro", 1, domain, denials, nd, kernel_count, NULL);
300    return nd == 0 ? 0 : 1;
301}