Saltar al contenido

audit

Evidencia de auditoría que un verificador puede comprobar offline

Por Olivares AI 13 min de lectura

Tu auditor pide evidencia de lo que hicieron tus agentes de IA el trimestre pasado. Le entregas un CSV del log de auditoría. Hace una sola pregunta: “Puedes demostrar que esto no se editó después?” La mayoría de equipos no pueden. El log vive en una base de datos mutable, exportado por el mismo sistema que lo escribió. El auditor tiene que confiar en toda la traza, que es exactamente la propiedad que hace que no sea evidencia.

Este artículo explica cómo la plataforma construye evidencia de auditoría que se sostiene sin fe. Un artículo anterior cubrió por qué la identidad por agente y un ledger a prueba de manipulación importan en despliegues de Claude Code y MCP. Este profundiza en las tres propiedades que hacen la evidencia verificable de forma independiente: hash-chaining, firma criptográfica por evento, y exportación en un formato de cumplimiento legible por máquina que las herramientas del auditor ya entienden.

La cadena de hashes: cada evento compromete a todos los anteriores

El ledger es append-only y hash-encadenado por tenant. Cada evento porta el hash SHA-256 del evento anterior en su campo prev_hash. El hash de cadena del evento N se computa sobre una preimagen binaria canónica que incluye: los propios campos del evento N (tenant, número de secuencia, timestamp, actor, acción, objetivo, digest de metadatos, hash del payload) concatenados con el prev_hash del evento N-1. En la secuencia 1, prev_hash es todo ceros — el ancla de génesis.

La propiedad fundamental: modificar, insertar o eliminar cualquier evento en medio de la cadena cambia su hash, lo que rompe el enlace prev_hash del siguiente evento, que rompe el siguiente, y así hasta la punta. Una sola edición se detecta recorriendo la cadena y recomputando cada hash.

La preimagen es una codificación binaria fija, con prefijos de longitud y un separador de dominio versionado — no JSON. Es una decisión deliberada contra tres superficies de ataque que un hash basado en JSON dejaría abiertas:

  • Orden de claves: el orden de claves en objetos JSON no está garantizado por la mayoría de serializadores. Un orden diferente produce un hash diferente incluso para datos semánticamente idénticos, lo que crea rupturas falsas en la cadena — o peor, permite a un atacante reordenar claves para forjar un hash coincidente.
  • Formato de espacios y números: {"seq": 1} y {"seq":1} y {"seq": 1.0} son JSON semánticamente equivalente pero producen digests SHA-256 diferentes.
  • Forja por concatenación: sin prefijos de longitud, dos campos cortos adyacentes pueden fusionarse para forjar un tercer campo largo con el mismo hash. Prefijar cada campo con su recuento de bytes (4 bytes big-endian) cierra este vector.

La implementación en core/internal/store/canon/canon.go es la fuente de verdad. Tanto Append (escritura) como Verify (lectura) llaman a la misma función EventHash. No hay una segunda implementación que pueda desincronizarse:

func EventHash(e Event) []byte {
    var buf []byte
    buf = lps(buf, domainEvent)       // "olivares.audit.v1"
    buf = lps(buf, e.TenantID)
    var seq [8]byte
    binary.BigEndian.PutUint64(seq[:], uint64(e.Seq))
    buf = append(buf, seq[:]...)
    buf = lps(buf, e.OccurredAt)
    buf = lps(buf, e.Actor)
    buf = lps(buf, e.ActorKind)
    buf = lps(buf, e.Action)
    buf = lps(buf, e.TargetKind)
    buf = lps(buf, e.TargetID)
    buf = append(buf, fixed(e.MetaDigest)...)
    buf = append(buf, fixed(e.PayloadHash)...)
    buf = append(buf, fixed(e.PrevHash)...)
    sum := sha256.Sum256(buf)
    return sum[:]
}

El separador de dominio "olivares.audit.v1" vincula el hash a su propósito y versión. Un hash de un dominio diferente (un checkpoint, un payload, un digest de metadatos) nunca puede colisionar con un hash de evento, aunque los bytes brutos coincidan.

Firma Ed25519: cada evento es su propio ancla

Una cadena de hashes demuestra consistencia interna, pero no autenticidad. Un atacante con acceso de escritura directo a la base de datos podría recomputar toda la cadena desde cero con eventos alterados y producir una cadena válida — diferente de la original, pero internamente consistente. Las cadenas de hashes detectan manipulación; no demuestran origen.

Las firmas Ed25519 por evento cierran esta brecha. Cada evento que se añade al ledger se firma en el momento de escritura. La firma cubre una preimagen separada por dominio del tenant, número de secuencia y el hash de cadena del evento:

domain ("olivares.audit.event.v1") || tenant || seq (8 bytes, big-endian) || hash

La firma se almacena en el evento pero se excluye de la preimagen del hash de cadena por diseño. No es accidental: si la firma se incluyese en el hash, firmar un evento cambiaría el hash que se supone que atestigua. La firma atestigua el hash sin alterarlo.

Un verificador externo que posea solo la clave pública puede confirmar cada evento individualmente: recomputa el hash de cadena a partir de los campos del evento, reconstruye la preimagen y verifica la firma Ed25519. Si algún evento fue alterado tras la firma, la comprobación falla para ese evento concreto — el verificador no necesita confiar en el sistema que produjo la evidencia.

La plataforma también soporta rotación de claves. Una cadena cuya clave de firma cambió a mitad de vida se verifica de extremo a extremo anclando la clave actual más las claves públicas de generaciones anteriores. La función de verificación acepta un conjunto de claves candidatas y considera un evento válido si cualquier candidata lo verifica.

Además de las firmas por evento, checkpoints periódicos notarizan la punta de la cadena bajo un dominio de firma separado (olivares.audit.checkpoint.v1). Para organizaciones que necesitan defenderse contra compromisos a nivel de host — no solo de base de datos — los checkpoints pueden firmarse con una clave KMS/HSM off-box (AWS KMS, GCP Cloud KMS, Azure Key Vault) donde la clave privada nunca reside en el host. Las firmas por evento cubren al atacante de BD; los checkpoints off-box cubren al atacante de host. Los dos modelos de amenaza son distintos; ninguna firma sola cubre ambos.

El contrato del ledger: sellado en la misma transacción

Un fallo habitual en sistemas de auditoría es la consistencia eventual entre la mutación de estado y el registro de auditoría. El estado cambia, la escritura de auditoría se encola o agrupa, y si la escritura de auditoría falla, el cambio de estado ya se ha confirmado. El resultado: mutaciones no auditadas que existen en el sistema pero no en la evidencia.

La plataforma impone un contrato más fuerte. Tanto la mutación de estado como el sellado en el ledger ocurren en la misma transacción de base de datos. Si el sellado falla, toda la transición se revierte — la mutación de estado nunca se confirma. No es best-effort; es deny-closed.

El ledger del runtime de sesiones lo ilustra. Cuando una sesión de Claude Code cambia de estado (created, launched, stopping, stopped, failed), appendRunEvent registra la transición en dos sitios de forma atómica:

  1. El ledger global de auditoría hash-encadenado mediante sc.Audit().Append — la cadena a prueba de manipulación anclada por un PayloadHash.
  2. El ledger consultable por sesión — una proyección append-only vinculada a la cadena global por audit_seq.

Ambas escrituras ocurren dentro de la transacción Mutate del llamador. El comentario del código en runtime_ledger.go declara la intención de diseño directamente: “the ledger is the system of record, so if the seal fails the whole transition rolls back — it is NOT best-effort.”

El propio PayloadHash solo compromete hechos canónicos y no sensibles de la transición — la referencia del run, la secuencia, el tipo de evento, la transición de estado y el timestamp. Nunca incluye contenido de transcripción, prompts, valores de entorno ni secretos. El ledger demuestra qué ocurrió; no almacena qué se dijo.

El mismo patrón aplica a las mutaciones de ficheros de workspace en workspace_ledger.go. Una escritura, mkdir, move o delete se sella antes de que la operación de sistema de ficheros se ejecute. Si la evidencia no puede añadirse, la mutación no se ejecuta. El sellado porta el tipo de operación, la ruta y un SHA-256 del contenido escrito — nunca los bytes del contenido.

Exportación OSCAL: evidencia legible por máquina que las herramientas del auditor ingieren

Un ledger a prueba de manipulación es necesario pero no suficiente para un auditor. Si la evidencia está en un formato propietario, el auditor sigue dependiendo de tus herramientas para interpretarla. OSCAL — Open Security Controls Assessment Language, mantenido por el NIST — es el formato que cierra esta brecha.

La plataforma exporta paquetes de evidencia sellados como un bundle OSCAL que contiene tres modelos:

  • Component definition: las capacidades del plano de control expresadas como requisitos implementados contra un framework de cumplimiento (NIST SP 800-53, ISO 27001, EU AI Act, y otros). Cada requisito implementado porta el ID del control, las claves de capacidad que lo evidencian y el estado real como propiedad personalizada.
  • Assessment results: hallazgos por control con un estado conforme a OSCAL. Cada hallazgo porta satisfied o not-satisfied con el estado preciso del producto preservado en el campo reason.
  • Control mapping: un cruce del framework de controles al modelo de referencia de capacidades de la plataforma, usando el modelo OSCAL 1.2.0 de control mapping. La relación es siempre intersects-with — las capacidades cubren parte de un control. Nunca asevera conformidad; esa aserción vive solo en assessment results, condicionada a evidencia operativa viva.

La restricción de honestidad en la exportación OSCAL merece mención explícita. El enum de estado de hallazgo de OSCAL tiene exactamente dos valores: satisfied y not-satisfied. No hay “parcial” ni “por diseño.” Un control parcialmente implementado, cubierto por diseño, con brecha o sin mapear se mapea a OSCAL not-satisfied, con el estado real del producto en status.reason y una propiedad personalizada bajo el namespace propio de la plataforma (https://olivares.ai/ns/oscal). La exportación nunca blanquea un control parcialmente cumplido como satisfied. Solo los controles respaldados por evidencia operativa viva en el momento del sellado reciben OSCAL satisfied.

Cada documento OSCAL porta propiedades de anclaje al ledger: el hash del manifiesto, el número de secuencia del ledger en el momento del sellado, el hash del ledger y el resultado de verificación de integridad. Son el puente entre el documento OSCAL que el auditor lee en su herramienta GRC y la cadena subyacente a prueba de manipulación que puede verificar de forma independiente.

Pipeline de actividad a evidencia

Qué significa “verificación offline” concretamente

“Verificación offline” no es una frase de marketing. Describe un procedimiento técnico concreto: el verificador toma la evidencia exportada, ejecuta una herramienta de verificación en una máquina air-gapped y confirma la integridad de la evidencia sin ningún acceso de red al sistema que la produjo.

La exportación archival de la plataforma escribe un árbol de directorios con segmentos JSONL (una línea por evento, JSON canónico) más un manifiesto por segmento. El manifiesto registra el rango de secuencias del segmento, el recuento de eventos, los hashes de cadena primero y último, un SHA-256 del fichero de eventos, y el último hash del segmento anterior para continuidad entre segmentos.

El verificador offline (VerifyArchiveDir) entonces ejecuta lo siguiente, enteramente en memoria constante y sin llamadas de red:

  1. Carga manifiestos y los empareja con ficheros de eventos. Un fichero de eventos sin manifiesto o un manifiesto cuyo fichero de eventos falta es un fallo. La unidad de evidencia es el par.
  2. Lee cada fichero de eventos línea a línea. Para cada evento, re-deriva el hash de cadena a partir de los campos archivados usando la misma función EventHash que usa el sistema en producción. Lo compara contra el hash almacenado. Comprueba el enlace prev_hash y la ausencia de saltos de secuencia.
  3. Verifica la canonicidad. Re-serializa cada línea parseada y confirma que produce una salida byte-idéntica a los bytes en disco. Esto previene ataques de contrabando de campos desconocidos o claves duplicadas que pasarían una comprobación de hash pero portarían datos ocultos.
  4. Verifica las firmas Ed25519 por evento. Para cada evento que no sea un checkpoint, reconstruye la preimagen de firma y verifica contra la(s) clave(s) pública(s) ancladas.
  5. Verifica las firmas de checkpoints. Para cada evento checkpoint, verifica la firma bajo el dominio de checkpoint contra la(s) clave(s) de checkpoint ancladas. Si solo se anclan claves de evento (sin clave de checkpoint), un evento checkpoint se marca como “no verificable” — deny-closed, no saltable.
  6. Comprueba la continuidad entre segmentos. Confirma que la primera secuencia de cada segmento sigue a la última secuencia del segmento anterior más uno, y que el último hash del segmento anterior coincide con el prev_segment_last_hash del segmento actual.
  7. Verifica el digest del fichero de eventos. El SHA-256 del fichero de eventos computado durante el streaming debe coincidir con el events_sha256 del manifiesto.

El verificador informa de la primera inconsistencia que encuentra, con el número de secuencia concreto y un motivo legible por máquina: hash-mismatch, prev-mismatch, seq-gap, event-sig-invalid, event-sig-missing, checkpoint-sig-invalid, count-mismatch, events-sha256-mismatch o segment-link-mismatch.

Una limitación honesta: el verificador offline atestigua exactamente el rango que comprobó y nada fuera de él. Un prefijo o cola eliminados son indetectables offline — el directorio no dice dónde empezó o terminó la cadena. El informe de verificación incluye un campo Ranges por tenant con un flag StartsMidChain para que el auditor sepa exactamente qué fue atestiguado. Los checkpoints firmados de la cadena en producción cubren la cola; la exportación offline cubre el rango archivado. Juntos forman la atestiguación completa.

CapaQué demuestraQué no demuestra
Cadena de hashesConsistencia interna; cualquier edición rompe la cadenaOrigen (quién escribió los eventos)
Ed25519 por eventoOrigen; cada evento fue firmado por el poseedor de la claveDefensa contra compromiso de host
Checkpoint off-boxResistencia a compromiso de host (clave KMS/HSM nunca en el host)Granularidad por evento (solo cubre checkpoints)
Exportación OSCALEvidencia de cumplimiento legible por máquina, mapeada a frameworksQue cada control se cumple totalmente (solo cuenta la evidencia viva)
Verificación archivalRe-derivación offline de todo lo anteriorEventos anteriores o posteriores al rango exportado

La ruta del código: de la mutación de estado a la evidencia sellada

La secuencia desde un cambio de estado de una sesión de Claude Code hasta evidencia verificable toca tres capas. En runtime_ledger.go, la función appendRunEvent construye un PayloadHash haciendo SHA-256 de los campos canónicos de transición con prefijos de longitud:

func runEventPayloadHash(runRef string, seq int64, event, from, to, detail, atTS string) [32]byte {
    h := sha256.New()
    for _, part := range []string{
        runRef, strconv.FormatInt(seq, 10), event, from, to, detail, atTS,
    } {
        _, _ = h.Write([]byte(strconv.Itoa(len(part))))
        _, _ = h.Write([]byte{':'})
        _, _ = h.Write([]byte(part))
    }
    var sum [32]byte
    copy(sum[:], h.Sum(nil))
    return sum
}

Ese hash se pasa a sc.Audit().Append, que asigna el siguiente número de secuencia por tenant, enlaza al hash del evento anterior, computa el hash de cadena de este evento mediante EventHash, lo firma con Ed25519 e inserta el evento sellado — todo dentro de la transacción del llamador.

El resultado: para cuando la transacción se confirma, el cambio de estado de la sesión y su registro de auditoría a prueba de manipulación están ambos persistidos o ambos revertidos. No hay ventana en la que el estado cambió pero la evidencia no.

Enlaces

Preguntas frecuentes

Un atacante que comprometa la base de datos, puede refirmar los eventos falsificados?

Las firmas Ed25519 por evento defienden contra compromisos exclusivos de base de datos: copias de seguridad robadas, filas inyectadas o una réplica con un rol que elude RLS. La clave de firma reside en el directorio de datos, no en la base de datos. Para el caso de compromiso a nivel de host, la plataforma soporta firma de checkpoints off-box mediante KMS/HSM (AWS KMS, GCP Cloud KMS, Azure Key Vault), donde la clave privada nunca reside en el host. Las firmas por evento detienen ataques a nivel de BD; los checkpoints off-box detienen los de nivel host. Ninguna de las dos cubre ambos modelos de amenaza por sí sola.

Qué ocurre si la exportación OSCAL marca un control como satisfied pero la evidencia operativa cambia después?

La exportación OSCAL mapea el estado del producto al enum de estado de hallazgo OSCAL en el momento del sellado. Solo los controles respaldados por evidencia operativa viva en ese instante reciben OSCAL satisfied. Un control con estado by_design, partial, gap o unmapped se mapea a OSCAL not-satisfied, con el estado preciso del producto preservado en el campo reason y una propiedad personalizada. Cada paquete de evidencia sellado es inmutable y tiene marca temporal. Si la evidencia cambia, se sella un paquete nuevo que refleja el estado actual. El paquete anterior permanece intacto y reverificable, creando una serie temporal de postura de cumplimiento en lugar de una aserción sobrescribible.

Mira a qué pueden llegar tus agentes

Olivares AI es la plataforma abierta y self-hosted para tu parque de IA. Despliégalo en tu propia infraestructura y obtén el mapa de acceso que tus equipos de seguridad y plataforma llevan pidiendo.