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:
- El ledger global de auditoría hash-encadenado mediante
sc.Audit().Append— la cadena a prueba de manipulación anclada por unPayloadHash. - 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
satisfiedonot-satisfiedcon 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.
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:
- 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.
- 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
EventHashque usa el sistema en producción. Lo compara contra el hash almacenado. Comprueba el enlaceprev_hashy la ausencia de saltos de secuencia. - 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.
- 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.
- 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.
- 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_hashdel segmento actual. - Verifica el digest del fichero de eventos. El SHA-256 del fichero de eventos computado durante el streaming debe coincidir con el
events_sha256del 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.
| Capa | Qué demuestra | Qué no demuestra |
|---|---|---|
| Cadena de hashes | Consistencia interna; cualquier edición rompe la cadena | Origen (quién escribió los eventos) |
| Ed25519 por evento | Origen; cada evento fue firmado por el poseedor de la clave | Defensa contra compromiso de host |
| Checkpoint off-box | Resistencia a compromiso de host (clave KMS/HSM nunca en el host) | Granularidad por evento (solo cubre checkpoints) |
| Exportación OSCAL | Evidencia de cumplimiento legible por máquina, mapeada a frameworks | Que cada control se cumple totalmente (solo cuenta la evidencia viva) |
| Verificación archival | Re-derivación offline de todo lo anterior | Eventos 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
- Producto: Audit — la traza de auditoría y la exportación de evidencia
- Producto: Compliance — evaluaciones de frameworks y OSCAL
- Modelo de seguridad — el modelo de confianza que sustenta el ledger