Una discusión sobre un archivo digital casi nunca se pierde por lo que el archivo dice. Se pierde una pregunta antes:
¿Cómo sabemos que ese es el archivo que usted recibió, y no el que editó anoche?
Si la respuesta es "confíe en mí", ya perdiste. Y da igual cuánta razón tengas en el fondo.
Este problema no es exclusivo de un juzgado. Lo tiene el auditor que recibe un volcado de logs, el equipo que documenta un incidente, quien conserva la copia de un contrato firmado por correo. En todos los casos hace falta lo mismo: poder demostrar que un conjunto de bytes no cambió desde un momento determinado, y que lo demuestre alguien que no seas tú.
Para eso escribí Tunjo: una herramienta en Rust que recorre un material en solo lectura, calcula su huella y firma un acta verificable por cualquiera.
Por qué un árbol y no un hash
Lo obvio sería concatenar todo y sacar un SHA-256. Funciona, y es inútil en la práctica.
Cuando alguien discute un archivo —un correo concreto entre cuatro mil— con un hash único solo puedes ofrecer dos cosas: o entregas el conjunto completo para que se recalcule, o pides que te crean. La primera opción expone material que no tiene por qué exponerse; la segunda no es una prueba.
Un árbol de Merkle resuelve exactamente eso. Cada archivo es una hoja, cada par de nodos se combina hacia arriba y queda una raíz. Para demostrar que una hoja pertenece a esa raíz basta con exhibir esa hoja y el camino de hashes hasta arriba: unos pocos kilobytes. El resto del conjunto no se toca.
Dos detalles del árbol que no son opcionales:
// Separación de dominio: una hoja nunca puede hacerse pasar por nodo interno.
h.update([0x00]); // hoja
h.update([0x01]); // nodo interno
// Y la raíz ata el número de hojas.
h.update([0x02]);
h.update(n.to_be_bytes());
Sin lo primero, un hash de hoja podría presentarse como si fuera un nodo del árbol. Sin lo segundo aparece la ambigüedad clásica de los árboles con número impar de hojas: dos conjuntos distintos pueden producir la misma raíz. Es un error viejo y conocido, y sigue apareciendo en implementaciones nuevas.
La huella cubre el estado, no solo el contenido
La hoja no es el hash del archivo: es el hash del elemento completo —ruta, tamaño, fecha, estado y hash del contenido—.
La diferencia importa. Si la huella fuera solo del contenido, mover un archivo de carpeta, renombrarlo o sustituirlo por un enlace que apunta al mismo contenido dejaría la raíz intacta. Y esos tres movimientos cambian lo que el conjunto significa: dónde estaba un documento es parte del hecho que se documenta, no un detalle de presentación.
Firmar para dentro de diez años
El acta se firma con la firma triple-híbrida de Quipu: Ed25519 + ML-DSA-87 (FIPS 204)
- SLH-DSA-SHA2-256s (FIPS 205), y las tres deben validar.
No es coleccionismo de algoritmos. Es que la vida útil de esto no se mide en meses: un expediente puede tardar años en resolverse, y la firma tiene que seguir verificándose al final. Las tres piezas fallan por motivos distintos —Ed25519 frente a un ordenador cuántico; ML-DSA por ser reciente y basada en retículos; SLH-DSA solo si se rompe la función hash— y hacen falta las tres a la vez para que el sello valga. Que caiga una no tumba el acta.
El coste es honesto: la firma pesa unos 34 KB. Para sellar un conjunto de archivos, es ruido.
Verificar la firma no basta
Este es el error que más fácil se comete al implementar algo así. La firma cubre el JSON completo del acta, incluida la raíz de integridad. Si al verificar te limitas a comprobar la firma, das por buena una raíz que nadie recalculó: alguien con la clave podría firmar un acta cuya raíz no corresponde a los elementos que lista, y pasaría el control.
Por eso la verificación recalcula el árbol siempre, y solo después mira la firma. Hay una prueba dedicada a ese caso exacto: firma auténtica sobre raíz mentirosa debe fallar.
$ tunjo verificar acta.json --origen ./evidencia
SELLO VÁLIDO
contenido: 4 elementos, 3 con contenido verificable
raíz: d9f6f68f591c6af087838dc27049a4194ab70525c350b79ec22446f9c12f9e33
1 DISCREPANCIA(S) contra evidencia:
ALTERADO adjuntos/c.pdf
acta: 3fdbaf9c795e22f14e16974c37b62ed381b9c8c4ac7bcbe1a01f13d08ec46643
disco: 9af5d94042eafbf2c335aa874085b263ff0201aee5e5033fd4c432e92de0093d
Ante la ausencia de un dato, ruido
Una herramienta de integridad que disimula sus fallos es peor que no tenerla, porque produce confianza sin respaldo. Tres decisiones al respecto:
Un archivo ilegible detiene el sellado. No se salta en silencio. Si de verdad es ilegible, hay que pedirlo explícitamente y entonces el acta lo registra como error: de ese elemento consta que existía y que la lectura falló, y nada más.
Del reloj se dice la verdad. El acta pide declarar cómo se contrastó con una fuente externa. Si no se declara, escribe "NO VERIFICADO" en lugar de callarlo. Sin sello de tiempo de un tercero, esto prueba orden relativo, no fecha cierta —y también lo dice.
Los enlaces simbólicos no se siguen. Se registra a dónde apuntan. Seguirlos sacaría la adquisición del material que se recibió.
Lo que deliberadamente no hace
No detecta intrusiones, no atribuye autoría y no concluye nada. Podría añadirle heurísticas que dijeran "aquí hubo un ataque", y sería un error: quien firma un informe tiene que poder defender cada afirmación línea por línea, y nadie defiende una heurística que no escribió. Cuando esa afirmación se cae, arrastra al resto del informe.
Tampoco prueba el pasado. Acredita desde el instante de la adquisición: si el material ya venía alterado, el sello certifica fielmente material alterado. Está escrito en el acta que genera, no en la letra pequeña.
El verificador es público, y no por generosidad
Si el único que puede comprobar un sello es quien lo emitió, no es una prueba: es una afirmación con formato técnico. Por eso el verificador es software libre y su código está publicado.
Lo comprobé de la única forma que vale: cloné el repositorio público en una máquina limpia, lo compilé desde cero y con ese binario verifiqué un acta sellada por otro. Válida. Después alteré un byte de un adjunto y el mismo binario señaló ese archivo y solo ese.
git clone https://github.com/isazajuancarlos/tunjo
cd tunjo && cargo build --release
./target/release/tunjo verificar acta.json --origen ./evidencia
Rust, AGPL-3.0, y las pruebas incluyen una simulación de 240 contrastes: se altera un byte de cada uno de 120 archivos y se exige que señale ese y solo ese, y que al restaurarlo no queden falsos positivos. Detectar es fácil; discriminar es el trabajo.
Xiliux