./research / verifiable-audit-layer
Uno strato di audit verificabile per l'IA.
Uno strato di audit a prova di manomissione per l'inferenza dei modelli e l'esecuzione degli agenti: serializzazione canonica, hashing con separazione di dominio, firme DSSE, un DAG di Merkle per esecuzione e una Merkle Mountain Range tra esecuzioni che prova che nessuna esecuzione è stata cancellata. Ogni primitiva è stata scelta da un benchmark diretto contro le vere alternative, con i dati grezzi versionati. Questa nota è la progettazione e gli esperimenti che le stanno dietro.
Modello di minaccia
Assumiamo un avversario che controlla l'archiviazione e può riscrivere i record a posteriori, incluso l'operatore che li ha prodotti. L'obiettivo è che ogni modifica a un'esecuzione sigillata sia rilevabile, che il record resti verificabile per un periodo di conservazione misurato in anni, e che la verifica non esponga alcun input privato. Due proprietà restano distinte per tutto il tempo: l'integrità di una singola esecuzione e la completezza dell'insieme delle esecuzioni. Una ricevuta per esecuzione dà la prima e non dice nulla sulla seconda, quindi entrambe sono progettate separatamente.
Ogni scelta qui sotto è stata fatta allo stesso modo. Non affermiamo che una primitiva sia la migliore; la confrontiamo con le alternative credibili sulla stessa macchina e versioniamo i dati grezzi, così ogni cifra qui si riproduce con un comando. I tempi sono il più veloce di più esecuzioni dopo aver scartato il riscaldamento, mantenendo media e deviazione standard perché una misura instabile sia segnalata anziché nascosta. I numeri provengono da un singolo host AMD64 su CPython 3.14 e sono significativi come rapporti, non come valori assoluti.
Esperimenti ed esiti
Ogni traguardo è stato un test diretto contro le vere alternative, non una singola misura. La panoramica è sotto; i risultati dettagliati di ciascuno sono nelle sezioni seguenti.
| Traguardo | Testato contro | Esito |
|---|---|---|
| Forma canonica | JSON ingenuo, CBOR | JCS, identico byte per byte, ~3x di costo |
| Hashing | SHA-256, BLAKE3, keccak-256 | keccak nativo 17x, stessi byte |
| Nucleo nativo | backend pip vs Rust | sigillatura 6x, radice identica byte per byte |
| Firma | ECDSA, BLS, ML-DSA | Ed25519, 64 B, deterministica |
| Post-quantistico | ML-DSA 44 / 65 / 87 | a fasi, verifica alla pari, 38x di dimensione |
| Traccia per esecuzione | 8 attacchi di falsificazione, overhead | tutti rilevati, ~121 us / evento |
| Log tra esecuzioni | ricostruzione Merkle semplice | MMR, append piatto, 483x |
Serializzazione canonica
Una capsula deve produrre lo stesso hash di byte nel nostro nucleo Python, nel nostro client TypeScript e nello strumento proprio di un revisore, altrimenti un record valido non passa la verifica. Usiamo RFC 8785 (JSON Canonicalization Scheme), che fissa la formattazione dei numeri, l'ordine delle chiavi per unità di codice UTF-16 e il rifiuto dei valori non finiti. L'abbiamo testato contro JSON a chiavi ordinate in modo ingenuo e contro CBOR.
| Approccio | Python vs Node | Costo |
|---|---|---|
| JSON ordinato ingenuo | diverge su Unicode e numeri | riferimento |
| RFC 8785 JCS | identico byte per byte | circa 3x nel serializzare |
| CBOR | binario, serve un decoder per leggerlo | compatto |
Il JSON ingenuo diverge tra runtime sull'ordinamento Unicode e sulla formattazione dei numeri, quindi rompe in silenzio la verifica tra linguaggi. JCS è identico byte per byte in entrambi, a circa 3x il tempo di serializzazione, pagato una volta per hash e trascurabile rispetto all'hashing e al lavoro di rete circostante. Ne teniamo esattamente un'implementazione. Quando il nostro modulo nativo ha portato il proprio serializzatore JSON, l'abbiamo rimosso, perché un secondo canonicalizzatore che concorda sugli input ordinari e diverge sui casi limite che un avversario può scegliere è peggio di nessuno.
Hashing e il nucleo nativo
Le foglie sono hashate con BLAKE3 e i nodi di Merkle con keccak-256, entrambi con separazione di dominio RFC 6962 (prefissi distinti per foglie e nodi interni) per chiudere la classe di seconda preimmagine in cui un nodo interno è presentato come foglia. keccak-256 non è SHA3-256; i due condividono una permutazione ma differiscono di un byte di padding, e il verificatore on-chain parla keccak, quindi una sostituzione silenziosa farebbe fallire ogni radice on-chain. Lo strato di hashing calcola perciò l'algoritmo richiesto o solleva un errore, mai un sosia, e registra l'algoritmo in ogni capsula così che un verificatore riproduca la funzione esatta invece di dedurla da ciò che è installato localmente.
| Primitiva, input da 64 B | Throughput | Nota |
|---|---|---|
| SHA-256 (stdlib) | 1,50M ops/s | riferimento, sempre presente |
| BLAKE3 (pip) | 1,44M ops/s | alla pari con SHA-256 |
| keccak-256 (pip) | 99k ops/s | 15x più lento, il collo di bottiglia |
| keccak-256 (nucleo Rust) | 1,69M ops/s | 17x più veloce di pip |
keccak non ha un'implementazione nella libreria standard, ed è ciò che ha reso il modulo nativo degno di essere costruito. Il divario è massimo su input di dimensione evento perché lì il costo di pycryptodome è l'allocazione di oggetti per chiamata e non l'hashing, esattamente la dimensione che una traccia di audit hasha di più; da capo a fondo, il nucleo nativo porta la sigillatura di un'esecuzione di 1000 eventi da 78 a 491 sigillature al secondo, circa 6x. Fondamentale: abilitare l'acceleratore non può cambiare il risultato: sigillare la stessa esecuzione con e senza la crate produce una radice identica bit per bit, una suite di conformità controlla entrambi i percorsi contro i vettori pubblicati dagli autori dell'algoritmo, e la CI costruisce una singola wheel ad ABI stabile (abi3) e verifica quell'unico artefatto su ogni Python da 3.10 a 3.14. Installarlo cambia la velocità e nient'altro.
Firme per un record decennale
Una firma su un record di audit deve reggere per tutto il tempo in cui il record può essere contestato, e gli obblighi di logging dell'IA si spingono ormai verso il decennio. Se lo schema si rompe mentre un record è ancora nella sua finestra di conservazione, un avversario può falsificarlo e retrodatarlo, quindi l'intero archivio fallisce in un colpo. Abbiamo valutato Ed25519 contro ECDSA, BLS e la famiglia post-quantistica NIST ML-DSA, su throughput e dimensioni.
| Schema | Verifica (ops/s) | Dimensione firma |
|---|---|---|
| ECDSA P-256 | 16 887 | ~72 B |
| ML-DSA-44 (post-quantistico) | 10 725 | 2420 B |
| Ed25519 | 10 375 | 64 B |
| BLS12-381 | solo impl. di riferimento | 96 B, aggrega |
Due risultati hanno ribaltato assunti che ci portavamo dietro, e li teniamo entrambi agli atti. Ed25519 non è il più veloce da verificare: ECDSA P-256 ha misurato circa 1,6x più veloce, su assembly di libreria ottimizzato. Ed25519 resta il default per dimensione della firma e per determinismo (RFC 8032), che elimina il guasto da riuso di nonce di ECDSA che fa trapelare una chiave privata mentre le firme ancora verificano. E le firme post-quantistiche non sono lente da verificare: ML-DSA-44 ha superato di poco Ed25519, con il costo nella firma (circa 13x più lento) e nella dimensione (circa 38x i byte). Poiché una traccia è firmata una volta e verificata per anni, il lato costoso è quello che facciamo di meno. BLS è stato scartato su un requisito, non su un numero: l'aggregazione collassa molte firme in 96 byte, ma un aggregato verifica tutto-o-niente e richiede ogni chiave e messaggio, il che è incompatibile con il divulgare un ramo di una decisione senza rivelare il resto.
Il riformulare che conta: le firme non sono riservate, quindi "raccogli ora, decifra dopo" non si applica, e la minaccia reale è la falsificazione retroattiva entro la finestra di conservazione. Ancorare una radice a un registro pubblico prova quando un record è esistito indipendentemente dallo schema di firma, quindi una chiave rotta costa la capacità di provare chi ha sigillato un record ma non quando o cosa. L'ancoraggio è perciò sul percorso critico e ML-DSA è messo in fila dietro di esso, con la busta DSSE mantenuta agile rispetto all'algoritmo e l'algoritmo registrato per capsula.
La traccia di audit per esecuzione
Ogni esecuzione è una sequenza di eventi solo-append e concatenata per hash (chiamate LLM, chiamate a strumenti, recuperi, spawn di sub-agenti, approvazioni umane, decisioni di guardrail), con una radice di Merkle su tutti e una firma DSSE sulla radice. Sono memorizzati solo hash di input e output, mai i payload, quindi la traccia preserva la privacy per default. Il verificatore non restituisce solo valido o non valido; nomina la garanzia che si è rotta.
| Operazione (esecuzione di 102 eventi) | Costo | Scalabilità |
|---|---|---|
| Registrare un evento | ~121 us | piatto, sotto lo 0,01% di un passo |
| Sigillare, costruire la radice | 0,15 ms | lineare negli eventi |
| Verificare, ricalcolare tutte le foglie | 9,2 ms | lineare, circa 60x la sigillatura |
Registrare è meno di un centesimo di punto percentuale di un tipico passo di agente, dominato dal round-trip del modello, quindi l'overhead non è motivo per campionare e la traccia resta completa anziché statistica. La verifica è molto più pesante della sigillatura perché ricalcola ogni foglia dal contenuto mentre la sigillatura costruisce solo l'albero su hash già presenti. Quell'asimmetria è corretta per una traccia di audit, dove registrare è costante e verificare è raro, e segna la riverifica in massa come il punto in cui il nucleo nativo e il parallelismo rendono dopo. Per rendere concrete le garanzie, una demo attacca un'esecuzione sigillata in otto modi, ciascuno respinto con una ragione specifica:
| Attacco | Respinto come |
|---|---|
| Riscrivere un output del modello | discordanza di hash di foglia |
| Cancellare o inserire un evento | discordanza del conteggio eventi |
| Riordinare gli eventi | rottura della catena |
| Troncare l'esecuzione | discordanza di testa della catena |
| Declassare l'algoritmo di hash | discordanza di hash di foglia |
| Presentare sotto la chiave sbagliata | la firma non verifica |
Il log di trasparenza tra esecuzioni
Una capsula per esecuzione valida non può provare che l'insieme delle esecuzioni sia completo. Un operatore che scarta un'esecuzione scomoda detiene una collezione di capsule singolarmente perfette e una storia con un buco. Lo chiudiamo con un singolo log solo-append di radici di capsula che supporta prove di inclusione (un'esecuzione è nel log) e prove di consistenza (il log a una dimensione precedente è un prefisso esatto del log attuale). La consistenza coglie la censura: un log ricostruito con un'esecuzione rimossa è internamente ben formato e ogni capsula superstite verifica ancora, ma fallisce un controllo di consistenza contro la radice pubblicata prima, e lo stesso controllo respinge un inserimento retrodatato. Questo funziona solo contro una radice fissata prima della manomissione, quindi quella radice deve vivere da qualche parte che l'operatore non possa rivedere, come un'ancora pubblica.
Usiamo una Merkle Mountain Range anziché un albero di Merkle semplice perché un append aggiunge solo nodi e non ne riscrive mai uno, il che lascia vivere il log su storage write-once o a oggetti e mantiene valide le vecchie prove di inclusione mentre il log cresce. Quella proprietà è quasi ottimale, non solo comoda: ePrint 2025/234 prova che ogni impegno succinto solo-append deve indurre un numero superlineare di aggiornamenti di testimone, e una variante vicina di MMR sostanzialmente raggiunge il limite.
| Dimensione del log | Costruzione MMR | Ricostruzione semplice |
|---|---|---|
| 500 | 1,3 ms | 158 ms (120x) |
| 1000 | 2,6 ms | 618 ms (237x) |
| 2000 | 5,2 ms | 2491 ms (483x) |
L'append resta piatto mentre il log cresce (circa 390k append al secondo sia a 100 sia a 10 000 voci) mentre ricostruire un albero semplice cala da 8176 a 84 radici al secondo nello stesso intervallo, la firma quadratica-contro-linearitmica. Lo storage si assesta a 2,00 hash memorizzati per voce e le prove di inclusione crescono solo da 7 a 17 passi tra 100 e 100 000 voci. Dove perdiamo: le nostre prove di consistenza sono O(log^2 n), portando un percorso per picco del vecchio log invece dell'O(log n) raggiungibile, pochi kilobyte a 100 000 voci e un compromesso deliberato per una costruzione che un revisore può controllare leggendola.
Limiti che non superiamo
L'hashing dà evidenza di manomissione, non immunità dalla manomissione. Un avversario che ricostruisce un'esecuzione e ne ricalcola la radice produce un record auto-consistente, perché una radice di Merkle prova che gli eventi corrispondono alla radice, non che la radice sia quella pubblicata. La firma coglie ciò, e l'ancora pubblica prova il momento. Il log di trasparenza poggia in grande sulla stessa assunzione: rileva un'esecuzione cancellata solo rispetto a una radice fissata prima della cancellazione. Dichiariamo questi confini nel prodotto e nelle sue demo, e un test asserisce che la demo di falsificazione continui a mostrare l'attacco che non fermiamo, così che il caso onesto non possa sparire in silenzio.
Riproducibilità
Ogni cifra qui viene da un benchmark versionato che si riesegue con un comando, accanto ai test, alle suite di conformità tra linguaggi e tra implementazioni, e alla matrice CI che esercita Python 3.10 fino a 3.14 con e senza il nucleo nativo. La metodologia, il JSON grezzo e le note di decisione (incluse le dimensioni in cui ogni scelta perde) sono nel repository, perché uno strato che rende l'IA auditabile deve essere auditabile esso stesso.