./research / verifiable-audit-layer
Uma camada de auditoria verificável para IA.
Uma camada de auditoria à prova de adulteração para inferência de modelos e execução de agentes: serialização canônica, hashing com separação de domínios, assinaturas DSSE, um DAG de Merkle por execução e uma Merkle Mountain Range entre execuções que prova que nenhuma execução foi apagada. Cada primitiva foi escolhida a partir de um benchmark direto contra as alternativas reais, com os dados brutos versionados. Esta nota é o projeto e os experimentos por trás dele.
Modelo de ameaça
Assumimos um adversário que controla o armazenamento e pode reescrever registros depois do fato, incluindo o operador que os produziu. O objetivo é que qualquer edição de uma execução selada seja detectável, que o registro permaneça verificável por um período de retenção medido em anos, e que a verificação não exponha nenhuma entrada privada. Duas propriedades são mantidas distintas o tempo todo: a integridade de uma única execução e a completude do conjunto de execuções. Um recibo por execução dá a primeira e nada diz sobre a segunda, então ambas são projetadas separadamente.
Cada escolha abaixo foi feita do mesmo modo. Não afirmamos que uma primitiva seja a melhor; nós a comparamos com as alternativas plausíveis na mesma máquina e versionamos os dados brutos, de modo que qualquer número aqui se reproduz em um comando. Os tempos são o mais rápido de várias execuções após descartar o aquecimento, com média e desvio padrão mantidos para que uma medição instável seja sinalizada em vez de escondida. Os números vêm de um único host AMD64 em CPython 3.14 e são significativos como razões, não como valores absolutos.
Experimentos e resultados
Cada marco foi um teste direto contra as alternativas reais, não uma medição única. A visão geral está abaixo; os resultados detalhados de cada um estão nas seções seguintes.
| Marco | Testado contra | Resultado |
|---|---|---|
| Forma canônica | JSON ingênuo, CBOR | JCS, idêntico byte a byte, ~3x de custo |
| Hashing | SHA-256, BLAKE3, keccak-256 | keccak nativo 17x, mesmos bytes |
| Núcleo nativo | backends do pip vs Rust | selagem 6x, raiz idêntica byte a byte |
| Assinatura | ECDSA, BLS, ML-DSA | Ed25519, 64 B, determinística |
| Pós-quântico | ML-DSA 44 / 65 / 87 | em etapas, verificação em pé de igualdade, 38x de tamanho |
| Trilha por execução | 8 ataques de falsificação, sobrecarga | todos detectados, ~121 us / evento |
| Log entre execuções | reconstrução de Merkle simples | MMR, anexação plana, 483x |
Serialização canônica
Uma cápsula precisa gerar o mesmo hash de bytes em nosso núcleo Python, em nosso cliente TypeScript e na própria ferramenta de um auditor, ou um registro válido falha na verificação. Usamos RFC 8785 (JSON Canonicalization Scheme), que fixa a formatação de números, a ordem das chaves por unidade de código UTF-16 e a rejeição de valores não finitos. Testamos contra JSON com chaves ordenadas de forma ingênua e contra CBOR.
| Abordagem | Python vs Node | Custo |
|---|---|---|
| JSON ordenado ingênuo | diverge em Unicode e números | referência |
| RFC 8785 JCS | idêntico byte a byte | cerca de 3x ao serializar |
| CBOR | binário, precisa de um decodificador para ler | compacto |
O JSON ingênuo diverge entre runtimes na ordenação Unicode e na formatação de números, então quebra em silêncio a verificação entre linguagens. O JCS é idêntico byte a byte em ambos, a cerca de 3x o tempo de serialização, pago uma vez por hash e desprezível diante do hashing e do trabalho de rede ao redor. Mantemos exatamente uma implementação dele. Quando nosso módulo nativo trouxe seu próprio serializador JSON, nós o removemos, porque um segundo canonicalizador que concorda em entradas comuns e diverge nos casos-limite que um adversário pode escolher é pior do que nenhum.
Hashing e o núcleo nativo
As folhas são hasheadas com BLAKE3 e os nós de Merkle com keccak-256, ambos com separação de domínios RFC 6962 (prefixos distintos para folhas e nós internos) para fechar a classe de segunda pré-imagem em que um nó interno é apresentado como folha. keccak-256 não é SHA3-256; os dois compartilham uma permutação mas diferem em um byte de preenchimento, e o verificador on-chain fala keccak, então uma substituição silenciosa faria toda raiz falhar on-chain. A camada de hashing, portanto, calcula o algoritmo solicitado ou lança um erro, nunca um sósia, e registra o algoritmo em cada cápsula para que um verificador reproduza a função exata em vez de inferi-la do que está instalado localmente.
| Primitiva, entrada de 64 B | Vazão | Nota |
|---|---|---|
| SHA-256 (stdlib) | 1,50M ops/s | referência, sempre presente |
| BLAKE3 (pip) | 1,44M ops/s | em pé de igualdade com SHA-256 |
| keccak-256 (pip) | 99k ops/s | 15x mais lento, o gargalo |
| keccak-256 (núcleo em Rust) | 1,69M ops/s | 17x mais rápido que o pip |
keccak não tem implementação na biblioteca padrão, o que fez valer a pena construir o módulo nativo. A diferença é maior em entradas do tamanho de um evento porque o custo do pycryptodome ali é a alocação de objetos por chamada e não o hashing, exatamente o tamanho que uma trilha de auditoria mais hasheia; de ponta a ponta, o núcleo nativo reduz a selagem de uma execução de 1000 eventos de 78 para 491 selagens por segundo, cerca de 6x. Crucial: habilitar o acelerador não pode mudar o resultado: selar a mesma execução com e sem a crate gera uma raiz idêntica bit a bit, uma suíte de conformidade confere ambos os caminhos contra os vetores publicados pelos autores do algoritmo, e a CI compila uma única wheel de ABI estável (abi3) e verifica esse único artefato em cada Python de 3.10 a 3.14. Instalá-lo muda a velocidade e nada mais.
Assinaturas para um registro de uma década
Uma assinatura sobre um registro de auditoria tem de valer por tanto tempo quanto o registro possa ser contestado, e as obrigações de logging de IA já caminham para a década. Se o esquema quebrar enquanto um registro ainda está em sua janela de retenção, um adversário pode falsificá-lo e retrodatá-lo, então o arquivo inteiro falha de uma vez. Avaliamos Ed25519 contra ECDSA, BLS e a família pós-quântica do NIST ML-DSA, em vazão e tamanhos.
| Esquema | Verificação (ops/s) | Tamanho da assinatura |
|---|---|---|
| ECDSA P-256 | 16 887 | ~72 B |
| ML-DSA-44 (pós-quântico) | 10 725 | 2420 B |
| Ed25519 | 10 375 | 64 B |
| BLS12-381 | apenas impl. de referência | 96 B, agrega |
Dois resultados derrubaram suposições que trazíamos, e mantemos ambos no registro. Ed25519 não é o mais rápido de verificar: ECDSA P-256 mediu cerca de 1,6x mais rápido, sobre assembly de biblioteca otimizado. Ed25519 continua sendo o padrão no tamanho da assinatura e no determinismo (RFC 8032), que remove a falha de reúso de nonce do ECDSA que vaza uma chave privada enquanto as assinaturas ainda verificam. E assinaturas pós-quânticas não são lentas de verificar: ML-DSA-44 superou por pouco o Ed25519, com o custo na assinatura (cerca de 13x mais lento) e no tamanho (cerca de 38x os bytes). Como uma trilha é assinada uma vez e verificada por anos, o lado caro é o que menos fazemos. BLS foi rejeitado por um requisito, não por um número: a agregação colapsa muitas assinaturas em 96 bytes, mas um agregado verifica tudo-ou-nada e precisa de cada chave e mensagem, o que é incompatível com divulgar um ramo de uma decisão sem revelar o resto.
O reenquadramento que importa: assinaturas não são confidenciais, então "colher agora, decifrar depois" não se aplica, e a ameaça real é a falsificação retroativa dentro da janela de retenção. Ancorar uma raiz a um livro-razão público prova quando um registro existiu independentemente do esquema de assinatura, então uma chave quebrada custa a capacidade de provar quem selou um registro, mas não quando ou o quê. A ancoragem está, portanto, no caminho crítico e o ML-DSA fica escalonado atrás dela, com o envelope DSSE mantido ágil quanto ao algoritmo e o algoritmo registrado por cápsula.
A trilha de auditoria por execução
Cada execução é uma sequência de eventos apenas-anexar e encadeada por hash (chamadas de LLM, chamadas de ferramentas, recuperações, criações de subagentes, aprovações humanas, decisões de guarda-corpo), com uma raiz de Merkle sobre todos e uma assinatura DSSE sobre a raiz. Só hashes de entradas e saídas são armazenados, nunca os payloads, então a trilha preserva a privacidade por padrão. O verificador não apenas retorna válido ou inválido; ele nomeia a garantia que se quebrou.
| Operação (execução de 102 eventos) | Custo | Escalonamento |
|---|---|---|
| Registrar um evento | ~121 us | plano, abaixo de 0,01% de um passo |
| Selar, construir a raiz | 0,15 ms | linear em eventos |
| Verificar, recomputar todas as folhas | 9,2 ms | linear, cerca de 60x a selagem |
Registrar é menos de um centésimo de por cento de um passo típico de agente, que é dominado pela ida e volta do modelo, então a sobrecarga não é razão para amostrar e a trilha permanece completa em vez de estatística. A verificação é muito mais pesada que a selagem porque recomputa cada folha a partir do conteúdo enquanto a selagem só constrói a árvore sobre hashes já presentes. Essa assimetria é a correta para uma trilha de auditoria, onde registrar é constante e verificar é raro, e marca a reverificação em massa como o ponto onde o núcleo nativo e o paralelismo compensam a seguir. Para tornar as garantias concretas, uma demo ataca uma execução selada de oito modos, cada um rejeitado com um motivo específico:
| Ataque | Rejeitado como |
|---|---|
| Reescrever uma saída do modelo | divergência de hash de folha |
| Apagar ou inserir um evento | divergência na contagem de eventos |
| Reordenar eventos | quebra de cadeia |
| Truncar a execução | divergência de cabeça de cadeia |
| Rebaixar o algoritmo de hash | divergência de hash de folha |
| Apresentar sob a chave errada | a assinatura não verifica |
O log de transparência entre execuções
Uma cápsula por execução válida não pode provar que o conjunto de execuções está completo. Um operador que descarta uma execução inconveniente detém uma coleção de cápsulas individualmente perfeitas e uma história com um buraco. Fechamos isso com um único log apenas-anexar de raízes de cápsula que suporta provas de inclusão (uma execução está no log) e provas de consistência (o log em um tamanho anterior é um prefixo exato do log atual). A consistência pega a censura: um log reconstruído com uma execução removida é internamente bem formado e cada cápsula sobrevivente ainda verifica, mas falha em uma checagem de consistência contra a raiz publicada antes, e a mesma checagem rejeita uma inserção retrodatada. Isso só funciona contra uma raiz comprometida antes da adulteração, então essa raiz precisa viver em algum lugar que o operador não possa revisar, como uma âncora pública.
Usamos uma Merkle Mountain Range em vez de uma árvore de Merkle simples porque uma anexação só adiciona nós e nunca reescreve um, o que deixa o log viver em armazenamento de escrita-única ou de objetos e mantém válidas as provas de inclusão antigas conforme o log cresce. Essa propriedade é quase ótima, não apenas conveniente: o ePrint 2025/234 prova que qualquer compromisso apenas-anexar sucinto deve induzir um número superlinear de atualizações de testemunha, e uma variante próxima de MMR essencialmente atinge o limite.
| Tamanho do log | Construção MMR | Reconstrução simples |
|---|---|---|
| 500 | 1,3 ms | 158 ms (120x) |
| 1000 | 2,6 ms | 618 ms (237x) |
| 2000 | 5,2 ms | 2491 ms (483x) |
A anexação permanece plana conforme o log cresce (cerca de 390k anexações por segundo tanto em 100 quanto em 10 000 entradas) enquanto reconstruir uma árvore simples cai de 8176 para 84 raízes por segundo na mesma faixa, a assinatura quadrática-versus-linearítmica. O armazenamento se estabiliza em 2,00 hashes guardados por entrada e as provas de inclusão crescem só de 7 para 17 passos entre 100 e 100 000 entradas. Onde perdemos: nossas provas de consistência são O(log^2 n), carregando um caminho por pico do log antigo em vez do O(log n) alcançável, alguns kilobytes em 100 000 entradas e uma troca deliberada por uma construção que um revisor pode conferir lendo.
Limites que não cruzamos
Hashing dá evidência de adulteração, não imunidade a ela. Um adversário que reconstrói uma execução e recomputa sua raiz produz um registro autoconsistente, porque uma raiz de Merkle prova que os eventos batem com a raiz, não que a raiz seja a que foi publicada. A assinatura pega isso, e a âncora pública prova o momento. O log de transparência repousa na mesma suposição no atacado: só detecta uma execução apagada em relação a uma raiz comprometida antes da exclusão. Declaramos esses limites no produto e em suas demos, e um teste garante que a demo de falsificação continue mostrando o ataque que não impedimos, para que o caso honesto não possa desaparecer em silêncio.
Reprodutibilidade
Cada número aqui vem de um benchmark versionado que roda de novo em um comando, ao lado dos testes, das suítes de conformidade entre linguagens e entre implementações, e da matriz de CI que exercita Python 3.10 a 3.14 com e sem o núcleo nativo. A metodologia, o JSON bruto e as notas de decisão (incluindo as dimensões em que cada escolha perde) estão no repositório, porque uma camada que torna a IA auditável tem de ser auditável ela mesma.