Il tuo revisore chiede prove di ciò che i tuoi agenti di intelligenza artificiale hanno fatto lo scorso trimestre. Consegna loro un’esportazione CSV del registro di controllo. Fanno una domanda: “Puoi provare che questo non è stato modificato dopo il fatto?” La maggior parte delle squadre non può. Il registro risiede in un database mutabile, esportato dallo stesso sistema che lo ha scritto. L’auditor deve percorrere l’intero percorso sulla base della fede, che è proprio la proprietà che la rende non prova.
Questo post spiega come la piattaforma costruisce prove di audit che reggono senza fede. Un post precedente spiega perché l’identità per agente e un registro a prova di manomissione sono importanti per le distribuzioni Claude Code e MCP. Questo approfondisce le tre proprietà che rendono le prove verificabili in modo indipendente: concatenamento di hash, firma crittografica per evento ed esportazione in un formato di conformità leggibile dalla macchina che gli strumenti di controllo già comprendono.
La catena hash: ogni evento si impegna a tutti gli eventi precedenti
Il registro è di sola aggiunta e concatenato tramite hash per tenant. Ogni evento porta l’hash SHA-256 dell’evento precedente nel suo campo prev_hash. L’hash della catena dell’evento N viene calcolato su una preimmagine binaria canonica che include: i campi dell’evento N (tenant, numero di sequenza, timestamp, attore, azione, destinazione, digest di metadati, hash del payload) concatenati con prev_hash dall’evento N-1. Nella sequenza 1, prev_hash è tutto zeri: l’ancora della genesi.
La proprietà critica: la modifica, l’inserimento o l’eliminazione di qualsiasi evento nel mezzo della catena ne modifica l’hash, che interrompe il collegamento prev_hash dell’evento successivo, che interrompe il successivo e così via fino alla punta. Una singola modifica è rilevabile percorrendo la catena e ricalcolando ciascun hash.
La preimmagine è una codifica binaria fissa con prefisso di lunghezza con un separatore di dominio con versione, non JSON. Questa è una scelta deliberata contro tre superfici di attacco che un hash basato su JSON lascerebbe aperte:
- Ordinamento delle chiavi: l’ordine delle chiavi degli oggetti JSON non è garantito dalla maggior parte dei serializzatori. Un diverso ordine di chiave produce un hash diverso anche per dati semanticamente identici, il che crea false interruzioni di catena o, peggio, consente a un utente malintenzionato di riordinare le chiavi per creare un hash corrispondente.
- Spazi bianchi e formattazione dei numeri:
{"seq": 1}e{"seq":1}e{"seq": 1.0}sono JSON semanticamente equivalenti ma producono digest SHA-256 diversi. - Falsificazione della concatenazione: senza prefissi di lunghezza, due campi brevi adiacenti possono essere uniti per creare un terzo campo lungo con lo stesso hash. Il prefisso di lunghezza di ciascun campo con il relativo conteggio di byte (big-endian a 4 byte) lo chiude.
L’implementazione in core/internal/store/canon/canon.go è l’unica fonte di verità. Sia Append (scrittura) che Verify (rilettura) chiamano la stessa funzione EventHash. Non esiste una seconda implementazione alla deriva:
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[:]
}
Il separatore di dominio "olivares.audit.v1" lega l’hash al suo scopo e alla sua versione. Un hash di un dominio diverso (un checkpoint, un payload, un digest di metadati) non può mai entrare in collisione con un hash di evento, anche se i byte grezzi corrispondono.
Firma Ed25519: ogni evento ha la sua ancora
Una catena di hash dimostra la coerenza interna, ma non l’autenticità. Un utente malintenzionato con accesso in scrittura al database non elaborato potrebbe ricalcolare l’intera catena da zero con eventi modificati e produrre una catena valida, diversa dall’originale, ma internamente coerente. Le catene hash rilevano manomissioni; non provano l’origine.
Le firme Ed25519 per evento chiudono questo gap. Ogni evento allegato al registro viene firmato al momento della scrittura. La firma copre una preimmagine separata dal dominio del tenant, del numero di sequenza e dell’hash della catena dell’evento:
domain ("olivares.audit.event.v1") || tenant || seq (8 bytes, big-endian) || hash
La firma viene archiviata nell’evento ma è esclusa dalla preimmagine dell’hash della catena in base alla progettazione. Ciò non è casuale: se la firma fosse inclusa nell’hash, la firma di un evento cambierebbe l’hash che dovrebbe attestare. La firma attesta l’hash senza alterarlo.
Un verificatore esterno che detiene solo la chiave pubblica può confermare ogni evento individualmente: ricalcolare l’hash della catena dai campi dell’evento, ricostruire la preimmagine e verificare la firma Ed25519. Se qualche evento è stato alterato dopo la firma, il controllo della firma fallisce per quell’evento specifico: il verificatore non ha bisogno di fidarsi del sistema che ha prodotto la prova.
La piattaforma supporta anche la rotazione delle chiavi. Una catena la cui chiave di firma è cambiata a metà del suo ciclo di vita verifica end-to-end bloccando la chiave corrente più le chiavi pubbliche delle generazioni precedenti. La funzione di verifica accetta un set di chiavi candidate e considera un evento valido se un candidato lo verifica.
Oltre alle firme per evento, i checkpoint periodici autenticano il suggerimento della catena in un dominio di firma separato (olivares.audit.checkpoint.v1). Per le organizzazioni che devono difendersi dalla compromissione a livello di host, non solo a livello di database, i checkpoint possono essere firmati da una chiave KMS/HSM off-box (AWS KMS, GCP Cloud KMS, Azure Key Vault) in cui la chiave privata non risiede mai sull’host. Le firme per evento gestiscono l’autore dell’attacco solo DB; I checkpoint off-box gestiscono l’aggressore che ha compromesso l’host. I due modelli di minaccia sono distinti; nessuna delle due firme da sola copre entrambe le cose.
Il contratto contabile: sigillato nella stessa transazione
Un fallimento comune nei sistemi di audit è l’eventuale coerenza tra la mutazione dello stato e la registrazione dell’audit. Lo stato cambia, la scrittura di controllo viene accodata o raggruppata e, se la scrittura di controllo fallisce, la modifica dello stato è già stata confermata. Il risultato: mutazioni non verificate che esistono nel sistema ma non nelle prove.
La piattaforma impone un contratto più forte. Sia la mutazione dello stato che il sigillo del registro avvengono nella stessa transazione del database. Se il sigillo fallisce, l’intera transizione torna indietro: la mutazione dello stato non viene mai eseguita. Questo non è il massimo sforzo; è negato-chiuso.
Il registro di runtime della sessione lo illustra. Quando una sessione Claude Code cambia stato (creata, avviata, in arresto, interrotta, non riuscita), appendRunEvent registra atomicamente la transizione in due punti:
- Il registro di controllo globale con catena hash tramite
sc.Audit().Append: la catena anti-manomissione ancorata aPayloadHash. - Il registro interrogabile per sessione: una proiezione di sola aggiunta collegata alla catena globale da
audit_seq.
Entrambe le scritture avvengono all’interno della transazione Mutate del chiamante. Il commento al codice in runtime_ledger.go dichiara direttamente l’intento progettuale: “il registro è il sistema di registrazione, quindi se il sigillo fallisce l’intera transizione viene ripristinata: NON è il massimo sforzo”.
Lo stesso PayloadHash si impegna solo a fatti di transizione canonici e non sensibili: riferimento all’esecuzione, sequenza, tipo di evento, transizione di stato e timestamp. Non include mai contenuto della trascrizione, prompt, valori dell’ambiente o segreti. Il registro dimostra cosa è successo; non memorizza ciò che è stato detto.
Lo stesso modello si applica alle mutazioni dei file dell’area di lavoro in workspace_ledger.go. Un file di scrittura, mkdir, spostamento o eliminazione viene sigillato prima dell’esecuzione dell’operazione sul filesystem. Se non è possibile aggiungere le prove, la mutazione non viene eseguita. Il sigillo riporta il tipo di operazione, il percorso e un SHA-256 del contenuto scritto, mai i byte del contenuto stesso.
Esportazione OSCAL: prove leggibili dalla macchina acquisite dagli strumenti del revisore
Un registro a prova di manomissione è necessario ma non sufficiente per un revisore. Se le prove sono in un formato proprietario, il revisore dipende comunque dai vostri strumenti per interpretarle. OSCAL - Open Security Controls Assessment Language, gestito dal NIST - è il formato che chiude questo gap.
La piattaforma esporta pacchetti di prove sigillati come un bundle OSCAL contenente tre modelli:
- Definizione del componente: le capacità del piano di controllo espresse come requisiti implementati rispetto a un quadro di conformità (NIST SP 800-53, ISO 27001, EU AI Act e altri). Ogni requisito implementato porta con sé l’ID di controllo, le chiavi di funzionalità che lo evidenziano e lo stato reale come proprietà personalizzata.
- Risultati della valutazione: risultati per controllo con stato conforme a OSCAL. Il target di ogni risultato riporta
satisfiedonot-satisfiedcon lo stato preciso del prodotto conservato nel campo del motivo. - Mappatura dei controlli: un passaggio dai controlli del framework al modello di riferimento delle capacità della piattaforma, utilizzando il modello di mappatura dei controlli OSCAL 1.2.0. La relazione è sempre
intersects-with: le funzionalità riguardano parte di un controllo. Non afferma mai la conformità; tale affermazione vive solo nei risultati della valutazione, basata su prove operative dal vivo.
Vale la pena menzionare esplicitamente il vincolo di onestà nell’esportazione OSCAL. L’enumerazione dello stato di ricerca di OSCAL ha esattamente due valori: satisfied e not-satisfied. Non esiste “partial” o “in base alla progettazione”. Un controllo partial implementato in modo corretto, indirizzato in base alla progettazione, gapped o unmapped si associa a OSCAL not-satisfied, con lo stato reale del prodotto in status.reason e una proprietà personalizzata nello spazio dei nomi della piattaforma (https://olivares.ai/ns/oscal). L’esportazione non ricicla mai un controllo partially-met in satisfied. Solo i controlli supportati da prove operative in tempo reale al momento del sigillo ricevono OSCAL satisfied.
Ogni documento OSCAL contiene proprietà di ancoraggio del registro: l’hash manifest, il numero di sequenza del registro al momento della sigillatura, l’hash del registro e il risultato della verifica dell’integrità. Si tratta del ponte tra il documento OSCAL che il revisore legge nel proprio strumento GRC e la catena di anti-manomissione sottostante che può verificare in modo indipendente.
Cosa significa concretamente la verifica offline
“Verifica offline” non è una frase di marketing. Descrive una procedura tecnica specifica: il verificatore prende le prove esportate, esegue uno strumento di verifica su una macchina air-gapped e conferma l’integrità delle prove senza alcun accesso di rete al sistema che le ha prodotte.
L’esportazione dell’archivio della piattaforma scrive un albero di directory di segmenti JSONL (una riga per evento, JSON canonico) più un manifest per segmento. Il manifest registra l’intervallo di sequenza del segmento, il conteggio degli eventi, il primo e l’ultimo hash della catena, un SHA-256 del file degli eventi e l’ultimo hash del segmento precedente per la continuità tra i segmenti.
Il verificatore offline (VerifyArchiveDir) esegue quindi quanto segue, interamente nella memoria costante senza chiamate di rete:
- Caricare i manifest e associarli ai file degli eventi. Un file di eventi sporadico (nessun manifest) o un manifest il cui file di eventi manca è un errore. L’unità di prova è la coppia.
- Trasmetti in streaming ogni file di evento riga per riga. Per ogni evento, deriva nuovamente l’hash della catena dai campi archiviati utilizzando la stessa funzione
EventHashutilizzata dal sistema live. Confrontalo con l’hash memorizzato. Controllare il collegamentoprev_hashe la libertà di sequenza gap. - Verificare la canonicità. Effettuare nuovamente il marshal di ciascuna riga analizzata e verificare che produca un output identico al byte sui byte su disco. Ciò impedisce il contrabbando di campi sconosciuti o gli attacchi con chiave duplicata che supererebbero un controllo hash ma trasporterebbero dati nascosti.
- Verifica le firme Ed25519 per evento. Per ogni evento non checkpoint, ricostruisci la preimmagine della firma e verificala con le chiavi pubbliche bloccate.
- Verifica le firme del checkpoint. Per ogni evento del checkpoint, verifica la firma nel dominio del checkpoint rispetto alle chiavi del checkpoint aggiunte. Se vengono bloccate solo le chiavi dell’evento (nessuna chiave del checkpoint), un evento del checkpoint viene contrassegnato come “non verificabile”: negato-chiuso, non ignorabile.
- Verifica la continuità tra i segmenti. Conferma che la prima sequenza di ciascun segmento segua l’ultima sequenza del segmento precedente più uno e che l’ultimo hash del segmento precedente corrisponda allo
prev_segment_last_hashdel segmento corrente. - Verificare il digest del file degli eventi. Il SHA-256 del file degli eventi calcolato durante lo streaming deve corrispondere al
events_sha256del manifest.
Il verificatore segnala la prima incoerenza trovata, con il numero di sequenza specifico e un motivo leggibile dalla macchina: hash-mismatch, prev-mismatch, seq-gap, event-sig-invalid, event-sig-missing, checkpoint-sig-invalid, count-mismatch, events-sha256-mismatch o segment-link-mismatch.
Un’onesta limitazione: il verificatore offline attesta esattamente l’intervallo controllato e nulla al di fuori di esso. Un prefisso o una coda rimossi non sono rilevabili offline: la directory non dice dove è iniziata o terminata la catena. Il rapporto di verifica include un campo Ranges per tenant con un flag StartsMidChain in modo che il revisore sappia esattamente cosa è stato attestato. I checkpoint firmati in catena del sistema live coprono la coda; l’esportazione offline copre l’intervallo archiviato. Insieme costituiscono l’attestazione completa.
| Strato | Cosa dimostra | Ciò che non dimostra |
|---|---|---|
| Catena di hash | Coerenza interna; qualsiasi modifica interrompe la catena | Origin (chi ha scritto gli eventi) |
| Per evento Ed25519 | Origine; ogni evento è stato firmato dal detentore delle chiavi | Difesa contro la compromissione a livello di host |
| Punto di controllo fuori scatola | Resistenza al compromesso dell’host (chiave KMS/HSM mai sull’host) | Granularità per evento (copre solo i checkpoint) |
| Esportazione OSCAL | Prove di conformità leggibili automaticamente e mappate nel framework | Che ogni controllo sia pienamente soddisfatto (contano solo le prove dal vivo) |
| Verifica dell’archivio | Nuova derivazione offline di tutto quanto sopra | Eventi prima o dopo l’intervallo esportato |
Il percorso del codice: dalla mutazione dello stato alla prova sigillata
La sequenza da un cambiamento dello stato della sessione Claude Code a una prova verificabile tocca tre livelli. In runtime_ledger.go, la funzione appendRunEvent costruisce un PayloadHash mediante l’hashing SHA-256 dei campi di transizione canonici con prefisso di lunghezza:
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
}
L’hash viene quindi passato a sc.Audit().Append, che assegna il numero di sequenza successivo per tenant, si collega all’hash dell’evento precedente, calcola l’hash della catena di questo evento tramite EventHash, lo firma con Ed25519 e inserisce l’evento sigillato, il tutto all’interno della transazione del chiamante.
Il risultato: al momento del commit della transazione, il cambiamento di stato della sessione e il relativo record di controllo anti-manomissione vengono entrambi mantenuti o entrambi sottoposti a rollback. Non esiste una finestra in cui lo stato sia cambiato ma le prove no.
Collegamenti
- Prodotto: Audit — traccia di controllo ed esportazione delle prove
- Prodotto: conformità — valutazioni quadro e OSCAL
- Modello di sicurezza — il modello di fiducia alla base del registro