# ¿Puede la verificación cifrada de logs de auditoría detectar eliminaciones?

Una cadena de hashes puede detectar que falta un registro en medio de un archivo de auditoría, pero no puede decirte con fiabilidad que alguien recortó registros del final. Esta diferencia hace que los equipos exageren lo que demuestra su verificador y descubran durante un incidente que conservaron pruebas de integridad, pero no el historial completo.

El cifrado no cambia esa respuesta. Protege el contenido de los registros. Una cadena construida sobre bytes cifrados puede demostrar que esos bytes siguen conectados como se esperaba, aunque nadie que ejecute el verificador pueda descifrarlos. No puede demostrar que el archivo contiene todos los registros que existieron alguna vez, salvo que una prueba externa al archivo conserve un punto posterior de su historia.

NIST describe una cadena de hashes como una estructura de solo adición en la que cada bloque incluye un hash de los datos anteriores. Por eso, modificar un bloque cambia el digest que registra su sucesor. Es evidencia de manipulación, no una máquina del tiempo.

## La verificación de la cadena comprueba una relación, no la integridad del historial

Un verificador comprueba si cada registro apunta al predecesor correcto y si el digest de cada registro coincide con sus bytes almacenados. Dado un primer registro de confianza, puede establecer la continuidad a través de todos los registros que siguen presentes.

Supón que un archivo contiene los registros del 1 al 100. El registro 58 incluye el digest del 57 y el 59 incluye el digest del 58. Si alguien elimina el 58 y deja todo lo demás intacto, el 59 sigue apuntando a un digest que el verificador no puede encontrar. La verificación falla en el hueco.

Ahora elimina los registros del 91 al 100. El 90 todavía apunta correctamente al 89. Nada dentro del archivo acortado dice que el 90 estuvo seguido alguna vez por diez registros más. Un verificador que empieza en el 1 y se detiene al terminar el archivo puede devolver éxito. Verificó la historia que se le presentó. No verificó que esa historia estuviera completa.

A menudo se agrupan tres afirmaciones distintas bajo «el log es resistente a manipulaciones»:

- Los registros que sobreviven no han cambiado.
- No se eliminó ningún registro del medio.
- El archivo termina donde terminaba la historia original.

Cada afirmación necesita pruebas distintas. Una cadena lineal gestiona bien la primera y detecta una versión sencilla de la segunda. La tercera necesita un compromiso independiente posterior, llamado a menudo checkpoint, cabecera, sello, recibo o testigo.

Certificate Transparency hace la misma separación con otra estructura de datos. RFC 9162 indica que una prueba de consistencia de Merkle puede demostrar que un árbol nuevo solo añade elementos respecto a un árbol anterior anunciado. La cabecera del árbol anterior hace aquí el trabajo real. Sin ella, una raíz actual no dice nada sobre los registros que el operador decidió no mostrarte.

## Crea un log de prueba que trate los payloads como ciphertext opaco

No practiques con un archivo de auditoría de producción. Crea un fixture desechable cuyos payloads parezcan ciphertext al verificador y prueba las eliminaciones sobre registros de bytes exactos.

El siguiente script escribe un archivo JSON Lines. Cada registro contiene un número de secuencia, un payload opaco en base64, el digest del predecesor y su propio digest. El payload son datos aleatorios de prueba, no cifrado real. Basta para esta prueba porque el verificador de la cadena solo necesita bytes opacos estables. En un log cifrado real, sustituye el registro de ciphertext serializado y sus metadatos autenticados.

```python
# make_log.py
import base64
import hashlib
import json
import os

OUT = "audit.jsonl"
ZERO = "0" * 64


def canonical(record):
    return json.dumps(record, sort_keys=True, separators=(",", ":")).encode()


def digest(record):
    return hashlib.sha256(canonical(record)).hexdigest()

prev = ZERO
with open(OUT, "w", encoding="utf-8") as f:
    for seq in range(1, 13):
        body = {
            "seq": seq,
            "ciphertext": base64.b64encode(os.urandom(24)).decode(),
            "prev": prev,
        }
        body["hash"] = digest(body)
        f.write(json.dumps(body, sort_keys=True) + "\n")
        prev = body["hash"]

print(f"wrote 12 records to {OUT}")
print(f"head seq=12 hash={prev}")
```

Usa un verificador separado en lugar de confiar en que el escritor valide su propia salida. Debe rechazar números de secuencia duplicados, saltos inesperados, registros mal formados, referencias incorrectas al predecesor y hashes incorrectos. Un verificador que solo comprueba hashes tiene un punto ciego: puede aceptar un archivo reordenado si el atacante también ha reorganizado suficientes campos asociados.

```python
# verify_log.py
import hashlib
import json
import sys

ZERO = "0" * 64


def canonical(record):
    return json.dumps(record, sort_keys=True, separators=(",", ":")).encode()


def digest_without_hash(record):
    copy = dict(record)
    supplied = copy.pop("hash", None)
    return supplied, hashlib.sha256(canonical(copy)).hexdigest()


def fail(message):
    print(f"FAIL {message}")
    raise SystemExit(1)

path = sys.argv[1]
prev = ZERO
expected_seq = 1
last_hash = ZERO

with open(path, encoding="utf-8") as f:
    for line_no, line in enumerate(f, start=1):
        try:
            record = json.loads(line)
        except json.JSONDecodeError:
            fail(f"line={line_no} invalid JSON")

        if record.get("seq") != expected_seq:
            fail(f"line={line_no} expected_seq={expected_seq} got={record.get('seq')}")

        if record.get("prev") != prev:
            fail(f"line={line_no} seq={expected_seq} predecessor mismatch")

        supplied, calculated = digest_without_hash(record)
        if supplied != calculated:
            fail(f"line={line_no} seq={expected_seq} digest mismatch")

        prev = supplied
        last_hash = supplied
        expected_seq += 1

print(f"OK records={expected_seq - 1} head={last_hash}")
```

Ejecuta el fixture y conserva la cabecera indicada fuera del archivo de log:

```text
python3 make_log.py
python3 verify_log.py audit.jsonl
```

El resultado debería tener este aspecto, con un digest distinto en cada ejecución:

```text
wrote 12 records to audit.jsonl
head seq=12 hash=8f...c2
OK records=12 head=8f...c2
```

El par final `records=12` y `head=...` es tu checkpoint. Guárdalo en un archivo separado de notas de prueba antes de modificar copias. Si dejas el checkpoint solo en el archivo que quieres atacar, has creado un registro cómodo de lo que el atacante puede editar.

## Eliminar el final muestra enseguida el límite

La eliminación del final es la prueba que debería hacer que todos sean prudentes con la palabra «completo». Copia el archivo, elimina las tres últimas líneas y ejecuta el mismo verificador.

```text
cp audit.jsonl tail-cut.jsonl
head -n 9 audit.jsonl > tail-cut.jsonl
python3 verify_log.py tail-cut.jsonl
```

El verificador informará de que nueve registros son válidos. Debe hacerlo. Los registros del 1 al 9 todavía forman una cadena válida. Llamar a esto un fallo del verificador fomentaría un diseño peligroso en el que los historiales parciales válidos parecen corruptos.

Compara el resultado con el checkpoint:

```text
OK records=9 head=4a...91
expected records=12 head=8f...c2
```

Ahora tienes pruebas de truncamiento. La prueba procede del desacuerdo con una evidencia capturada cuando el log era más largo, no de los enlaces internos del archivo acortado.

Un número de secuencia ayuda a diagnosticar el problema, pero no crea seguridad por sí solo. Un atacante que pueda reescribir el final puede cambiar el número final de 12 a 9. El número resulta útil cuando el valor esperado proviene de una fuente que el atacante no puede reescribir, como un checkpoint firmado almacenado por otro servicio, un artefacto de release protegido o un export que llegó a otro dominio administrativo.

Esta es la prueba que muchos diseños de auditoría omiten porque parece demasiado obvia. También es el fallo que importa después de una acción destructiva. Un proceso malicioso no necesita volver internamente incoherente la historia. Solo tiene que hacer que termine lo bastante pronto para ocultar la acción.

## Una eliminación intermedia debería fallar, pero solo bajo condiciones concretas

Haz otra copia y elimina un registro que tenga tanto predecesor como sucesor. El registro 6 es un buen objetivo.

```text
cp audit.jsonl middle-cut.jsonl
sed '6d' audit.jsonl > middle-cut.jsonl
python3 verify_log.py middle-cut.jsonl
```

El resultado debería tener este formato:

```text
FAIL line=6 expected_seq=6 got=7
```

Si eliminas la comprobación de secuencia, el verificador fallará una prueba después, porque el registro 7 contiene el digest del 6 mientras el verificador tiene el digest del 5. Conserva ambas comprobaciones. El desajuste de secuencia hace que el problema resulte evidente para el operador y el desajuste del predecesor muestra qué relación criptográfica se rompió.

No afirmes que una cadena de hashes siempre detecta la eliminación intermedia. Detecta una eliminación sencilla cuando el atacante no puede reescribir la cadena restante. Cualquiera puede calcular nuevos hashes con un algoritmo público. Si alguien puede eliminar el registro 6, cambiar el campo de predecesor del 7, recalcular su hash y repetir el proceso hasta el 12, el archivo reconstruido puede verificarse contra su propia cabecera nueva.

Esto no es un ataque de colisión ni una ruptura de SHA-256. Es un recálculo normal. El sistema aceptó una historia nueva porque no tenía pruebas protegidas de que la historia anterior hubiera existido.

La orientación de NIST sobre pruebas digitales expresa claramente el punto operativo: un hash almacenado debe vivir donde las personas con acceso a las pruebas no puedan alterarlo ni sobrescribirlo. El mismo informe considera útiles las cadenas de hashes para proteger hashes, pero sigue siendo necesario guardar externamente el valor de referencia.

Por tanto, una prueba de eliminación intermedia tiene dos versiones:

1. Elimina una línea y deja intactos los bytes posteriores. La cadena debe fallar.
2. Elimina una línea y regenera todos los digests posteriores. La cadena reconstruida debe fallar frente a un checkpoint conservado de forma independiente.

Si tu plan solo ejecuta la primera versión, prueba daños en el archivo y manipulaciones descuidadas. No prueba a un atacante que puede escribir en el almacenamiento del log.

## El cifrado y la integridad de la cadena responden preguntas distintas

Los registros cifrados crean una división útil de responsabilidades. Los operadores pueden verificar la cadena sobre ciphertext sin leer secretos, cuerpos de solicitudes, salidas de comandos ni otros contenidos sensibles de auditoría. Los investigadores con el acceso adecuado pueden descifrar después los registros relevantes. Es un diseño razonable cuando los propios datos de auditoría son sensibles.

Pero los bytes cifrados no certifican su propio origen. Un registro de ciphertext puede eliminarse. Alguien que controle la ruta de cifrado y registro puede añadir otro. Si el modo de cifrado no autentica los datos, a veces puede alterarse un ciphertext hasta convertirlo en basura sin que el descifrador sepa por qué. Usa cifrado autenticado para la confidencialidad e integridad de cada registro y usa después la cadena para unirlos en una historia ordenada.

Separa estas comprobaciones al redactar el diseño y el informe del incidente:

- La autenticación del registro pregunta si un registro cifrado fue alterado.
- La verificación de la cadena pregunta si cada registro que sobrevive sigue al predecesor declarado.
- La comparación con un checkpoint pregunta si la historia observada alcanza una cabecera observada anteriormente.
- La captura de eventos pregunta si el sistema llegó a escribir la acción en el log.

La última pregunta es incómoda. Ningún log criptográfico puede demostrar que un evento se registró si un escritor comprometido decidió no emitirlo. Puedes reducir esa exposición recopilando pruebas en fronteras independientes, como un servicio de red, una instalación de auditoría del host o una aprobación humana. No puedes eliminar la brecha llamando append-only al log local.

El modelo de auditoría de Sallyport resulta útil aquí porque sus journals de ejecuciones de agentes y llamadas individuales proceden de un único log de auditoría cifrado y encadenado con hashes, y `sp audit verify` puede verificar sin conexión la cadena de ciphertext sin una clave del vault. Así, un revisor puede comprobar la continuidad sin exponer las credenciales ni los datos de acción que protege el historial.

## Un checkpoint debe ser difícil de reescribir, no solo una copia

Un checkpoint es una declaración sobre un log en un momento concreto. Como mínimo, incluye un identificador del log, el número de secuencia final, el digest final, la hora de creación y la versión del formato o algoritmo. Trátalo como un artefacto separado con su propia custodia.

Este formato pequeño basta para un arnés de pruebas:

```json
{
  "log_id": "agent-actions-test-a",
  "sequence": 12,
  "head": "8f...c2",
  "created_at": "2026-07-22T14:30:00Z",
  "format": "jsonl-chain-v1"
}
```

No confíes por sí sola en la propiedad `created_at`. Un reloj local sirve para ordenar e investigar, pero un atacante que controle la máquina puede controlar el reloj y el archivo que contiene el valor. El checkpoint obtiene su fuerza del lugar al que se envió y de quién puede cambiarlo.

Una disposición práctica hace que una parte escriba los registros de auditoría y otra conserve cabeceras periódicas. La segunda parte no necesita acceso para descifrar. Necesita información suficiente para rechazar un archivo posterior cuyo digest final y número de secuencia no coincidan con lo que vio antes.

En una herramienta para desarrolladores, la parte independiente puede ser modesta: un artefacto protegido de CI, un sistema de attestations de release, una cuenta de recopilación separada o un export diario aprobado por otro administrador. Para acciones de mayor riesgo, emite el checkpoint inmediatamente después de la acción y consérvalo fuera de la estación de trabajo. La frecuencia debe seguir la ventana de daño que puedas aceptar. Un checkpoint diario no puede demostrar que el historial está completo durante las horas entre la cabecera de ayer y el final perdido de hoy.

Firmar un checkpoint ayuda cuando los verificadores necesitan saber quién lo emitió. La firma vincula los datos del checkpoint con la autoridad firmante, pero no hace honesto al firmante. Si el mismo proceso comprometido escribe el log y firma checkpoints de reemplazo, sigues teniendo una sola frontera de confianza. Coloca el testigo donde el escritor original no pueda controlarlo en silencio.

## El almacenamiento write-blind cambia el atacante que estás probando

Una cadena almacenada en un lugar donde el escritor puede editar libremente registros antiguos tiene un modelo de amenazas distinto del de una cadena almacenada mediante una ruta de adición write-blind. El primer diseño necesita testigos externos para detectar reescrituras. El segundo intenta impedir que el escritor las haga.

Sé preciso con la expresión «write-blind». Debe significar que el componente que envía un registro nuevo no puede leer ni modificar a voluntad los registros cifrados anteriores. No significa que el disco no pueda fallar, que un administrador no pueda eliminar un archivo o que el sistema operativo no pueda verse comprometido. Reduce una vía de ataque: reescribir una parte concreta de la historia después de haberla visto.

Esa diferencia afecta a los casos de prueba. Para el acceso normal a archivos, prueba la eliminación seguida del recálculo de hashes, porque un atacante puede leer los datos y calcular hashes. Para un almacén write-blind, prueba si un usuario puede solicitar una eliminación, sustitución, reversión o un segundo log con la misma identidad. Prueba también cómo identifica el verificador el log correcto. Una cadena perfecta del archivo equivocado sigue siendo una prueba equivocada.

El error más habitual es tratar los permisos de almacenamiento como un ancla criptográfica. Un atacante con privilegios suficientes puede cambiar los permisos. Siguen siendo importantes porque reducen los daños casuales y limitan qué procesos pueden actuar, pero no sustituyen un checkpoint que haya cruzado una frontera que el atacante no controle.

## Prueba la reversión por separado del truncamiento

Una reversión parece un truncamiento del final con un archivo antiguo plausible. Una máquina puede restaurar el archivo de auditoría válido de ayer después de un fallo, una restauración de backup o un reemplazo deliberado. El archivo restaurado puede pasar su verificador interno porque ayer era realmente válido.

Tu checkpoint detecta esto si registra una cabecera más nueva. Compara el archivo candidato con el checkpoint más reciente conservado, no con el más antiguo que encuentres. Un verificador de cadenas no puede inferir qué versión válida debería ser la actual.

Pruébalo directamente:

1. Guarda `audit.jsonl` y su checkpoint de 12 registros.
2. Genera otro fixture con más registros y conserva su checkpoint más reciente.
3. Sustituye el archivo más nuevo por la copia de 12 registros.
4. Verifica internamente el archivo restaurado y compáralo después con el checkpoint más reciente.

Espera dos resultados distintos. La verificación interna debe tener éxito. La comparación con el checkpoint debe fallar porque el archivo termina en una secuencia y un digest antiguos. Si fallan ambas comprobaciones, quizá tu fixture haya cambiado bytes en vez de reproducir una reversión. Si ambas tienen éxito, comparas con el checkpoint equivocado o no conservaste ninguno.

No «corrijas» una reversión añadiendo registros nuevos al archivo antiguo y continuando. Conserva primero el estado. Al añadir algo, mezclas una historia potencialmente incompleta con actividad posterior al incidente. Empieza un segmento de log nuevo con una frontera de incidente registrada o sigue el procedimiento de conservación y recuperación aprobado por tu organización.

## Haz que los resultados del verificador sirvan durante una investigación

Un verificador que solo devuelve `invalid` crea trabajo innecesario. Debe identificar la primera condición fallida sin imprimir los payloads sensibles de los registros. El número de secuencia, el desplazamiento de bytes o número de línea, el digest esperado del predecesor, el observado y el digest calculado del registro suelen aportar suficiente detalle.

Evita registrar contenido descifrado en un mensaje de error. Una herramienta de verificación suele ejecutarse en CI, en un paquete de soporte o en una captura del terminal. Si un diseño de auditoría cifrada filtra texto claro cada vez que falla la verificación, convierte un incidente de integridad en una exposición de datos.

Una respuesta disciplinada ante un fallo de verificación sigue un orden breve:

- Conserva el archivo original y registra el digest de esa copia.
- Recopila el checkpoint más reciente, los anteriores y cualquier export externo.
- Ejecuta la verificación sobre copias y guarda la salida exacta del comando.
- Compara la cabecera indicada con todos los checkpoints conservados.
- Determina si el fallo es corrupción, un hueco intermedio, pérdida del final, reversión o una cadena reescrita que entra en conflicto con un testigo.

La clasificación importa. Un fallo por hueco intermedio indica una contradicción interna. Una cadena válida pero más corta indica que el archivo quizá solo esté completo hasta su último registro. Una cadena válida que no coincide con un checkpoint posterior aporta pruebas de reversión, truncamiento o sustitución. Una cadena reescrita puede parecer limpia por dentro, pero no coincidirá con una cabecera independiente anterior.

No prometas más de lo que permiten las pruebas. «La cadena de auditoría se verificó hasta la secuencia 90 y no coincide con el checkpoint conservado de la secuencia 100» es una afirmación sólida y específica. «Nadie cambió el log» no lo es.

## La prueba que cuenta es la que se enfrenta a tu frontera real

Ejecuta primero las pruebas desechables de eliminación de líneas, porque enseñan la mecánica. Después repite los mismos casos contra el export de auditoría real y su verificador real, usando copias y los límites de manipulación documentados por el formato. No edites un log cifrado de producción en el mismo sitio solo para ver qué ocurre.

En Sallyport, conserva el log cifrado original y usa `sp audit verify` sobre una copia antes y después de cada alteración controlada. Registra si el verificador detecta un hueco interno, si una copia acortada sigue verificándose y si tu cabecera conservada revela la historia más corta. La respuesta a las tres preguntas es más útil que una afirmación vaga de que el log ofrece evidencia de manipulación.

Una cadena válida significa que los registros comprobados coinciden entre sí. Una cadena válida más un checkpoint posterior de confianza significa que los registros alcanzan un punto conocido de la historia. Diseña y prueba para la segunda afirmación cuando importe detectar acciones ausentes.
