# A verificação de logs de auditoria criptografados detecta exclusões?

Uma cadeia de hashes pode detectar um registro ausente no meio de um arquivo de auditoria, mas não consegue informar com segurança que alguém removeu registros do final. Essa diferença faz com que equipes exagerem o que seu verificador comprova. Depois, durante um incidente, elas descobrem que preservaram evidências de integridade, mas não o histórico completo.

A criptografia não muda essa resposta. Ela protege o conteúdo dos registros. Uma cadeia construída sobre bytes criptografados pode mostrar que esses bytes continuam conectados como esperado, mesmo quando ninguém que executa o verificador consegue descriptografá-los. Ela não prova que o arquivo contém todos os registros que já existiram, a menos que alguma evidência externa ao arquivo guarde um ponto posterior do histórico.

O NIST descreve uma cadeia de hashes como uma estrutura somente de acréscimo em que cada bloco inclui um hash dos dados anteriores. Assim, alterar um bloco muda o digest registrado pelo bloco seguinte. Isso fornece evidência de adulteração, não uma máquina do tempo.

## A verificação da cadeia confere uma relação, não a completude

O verificador confere se cada registro aponta para o predecessor correto e se o digest de cada registro corresponde aos bytes armazenados. Dado um primeiro registro confiável, ele consegue estabelecer a continuidade por todos os registros que ainda aparecem depois dele.

Suponha que um arquivo contenha os registros de 1 a 100. O registro 58 inclui o digest do registro 57, e o registro 59 inclui o digest do registro 58. Se alguém excluir o registro 58, deixando todo o resto intacto, o registro 59 continuará apontando para um digest que o verificador não encontra. A verificação falha na lacuna.

Agora remova os registros de 91 a 100. O registro 90 continua apontando corretamente para o registro 89. Nada dentro do arquivo encurtado diz que o registro 90 já foi seguido por outros dez registros. Um verificador que começa no registro 1 e para quando o arquivo termina pode retornar sucesso. Ele verificou o histórico apresentado. Não verificou se esse histórico estava completo.

É comum condensar três afirmações diferentes em «o log é à prova de adulteração»:

- Os registros restantes não foram alterados.
- Nenhum registro foi removido do meio.
- O arquivo termina onde o histórico original terminou.

Essas afirmações exigem evidências diferentes. Uma cadeia linear trata bem da primeira e detecta uma versão simples da segunda. A terceira precisa de um compromisso independente posterior, muitas vezes chamado de checkpoint, head, selo, recibo ou testemunha.

O Certificate Transparency faz a mesma separação com outra estrutura de dados. A RFC 9162 diz que uma prova de consistência de Merkle pode provar que uma árvore mais nova é somente de acréscimo em relação a uma árvore anterior anunciada. O head da árvore anterior anunciada tem uma função real nesse processo. Sem ele, uma raiz atual não diz nada sobre registros que o operador do log decidiu não mostrar.

## Crie um log de teste que trate os payloads como texto cifrado opaco

Não pratique em um arquivo de auditoria de produção. Crie um artefato descartável cujos payloads pareçam texto cifrado para o verificador e teste a exclusão usando registros com bytes exatos.

O script a seguir grava um arquivo JSON Lines. Cada registro contém um número de sequência, um payload opaco em base64, o digest do predecessor e seu próprio digest. O payload é deliberadamente um dado de teste aleatório, não uma criptografia real. Isso basta para este teste, porque o verificador da cadeia precisa apenas de bytes opacos estáveis. Em um log criptografado real, substitua esse valor pelo registro de texto cifrado serializado e seus metadados autenticados.

```python
# make_log.py
import base64
import hashlib
import json
import os

OUT = "audit.jsonl"
ZERO = "0" * 64


def canonical(record):
    return json.dumps(record, sort_keys=True, separators=(",", ":")).encode()


def digest(record):
    return hashlib.sha256(canonical(record)).hexdigest()

prev = ZERO
with open(OUT, "w", encoding="utf-8") as f:
    for seq in range(1, 13):
        body = {
            "seq": seq,
            "ciphertext": base64.b64encode(os.urandom(24)).decode(),
            "prev": prev,
        }
        body["hash"] = digest(body)
        f.write(json.dumps(body, sort_keys=True) + "\n")
        prev = body["hash"]

print(f"wrote 12 records to {OUT}")
print(f"head seq=12 hash={prev}")
```

Use um verificador separado, em vez de confiar no escritor para validar a própria saída. Ele deve rejeitar números de sequência duplicados, saltos inesperados na sequência, registros malformados, referências incorretas ao predecessor e hashes incorretos. Um verificador que confira apenas hashes tem um ponto cego: pode aceitar um arquivo reordenado se o invasor também tiver reorganizado campos associados suficientes.

```python
# verify_log.py
import hashlib
import json
import sys

ZERO = "0" * 64


def canonical(record):
    return json.dumps(record, sort_keys=True, separators=(",", ":")).encode()


def digest_without_hash(record):
    copy = dict(record)
    supplied = copy.pop("hash", None)
    return supplied, hashlib.sha256(canonical(copy)).hexdigest()


def fail(message):
    print(f"FAIL {message}")
    raise SystemExit(1)

path = sys.argv[1]
prev = ZERO
expected_seq = 1
last_hash = ZERO

with open(path, encoding="utf-8") as f:
    for line_no, line in enumerate(f, start=1):
        try:
            record = json.loads(line)
        except json.JSONDecodeError:
            fail(f"line={line_no} invalid JSON")

        if record.get("seq") != expected_seq:
            fail(f"line={line_no} expected_seq={expected_seq} got={record.get('seq')}")

        if record.get("prev") != prev:
            fail(f"line={line_no} seq={expected_seq} predecessor mismatch")

        supplied, calculated = digest_without_hash(record)
        if supplied != calculated:
            fail(f"line={line_no} seq={expected_seq} digest mismatch")

        prev = supplied
        last_hash = supplied
        expected_seq += 1

print(f"OK records={expected_seq - 1} head={last_hash}")
```

Execute o artefato e guarde o head informado fora do arquivo de log:

```text
python3 make_log.py
python3 verify_log.py audit.jsonl
```

O resultado de sucesso deve ter esta aparência, com um digest diferente a cada execução:

```text
wrote 12 records to audit.jsonl
head seq=12 hash=8f...c2
OK records=12 head=8f...c2
```

Esse par final, `records=12` e `head=...`, é o seu checkpoint. Coloque-o em um arquivo separado de anotações do teste antes de alterar cópias. Se você deixar o checkpoint apenas no arquivo que pretende atacar, terá criado um registro conveniente do que o invasor pode editar.

## A remoção do final mostra imediatamente o limite

A exclusão no final é o teste que deve deixar todos cautelosos com a palavra «completo». Copie o arquivo, remova as três últimas linhas e execute o mesmo verificador.

```text
cp audit.jsonl tail-cut.jsonl
head -n 9 audit.jsonl > tail-cut.jsonl
python3 verify_log.py tail-cut.jsonl
```

O verificador informará sucesso para nove registros. É isso que deve acontecer. Os registros de 1 a 9 ainda formam uma cadeia válida. Chamar isso de falha do verificador incentivaria um projeto perigoso, no qual históricos parciais válidos parecem corrompidos.

Compare o resultado com o checkpoint:

```text
OK records=9 head=4a...91
expected records=12 head=8f...c2
```

Agora você tem evidência de truncamento. A prova vem da discordância com uma evidência capturada quando o log era mais longo, não das ligações internas do arquivo encurtado.

Um número de sequência ajuda no diagnóstico humano, mas não cria segurança por si só. Um invasor que possa reescrever o final pode mudar o número de sequência final de 12 para 9. O número se torna útil quando o valor esperado vem de uma fonte que o invasor não pode reescrever, como um checkpoint assinado armazenado por um serviço separado, um artefato de release protegido ou uma exportação que chegou a outro domínio administrativo.

Esse é o teste que muitos projetos de auditoria ignoram por parecer óbvio demais. Também é a falha que importa depois de uma ação destrutiva. Um processo malicioso não precisa tornar o histórico internamente inconsistente. Basta fazer o histórico parar cedo o suficiente para esconder a ação.

## Uma exclusão no meio deve falhar, mas apenas sob condições declaradas

Crie outra cópia e exclua um registro que tenha predecessor e sucessor. O registro 6 é um bom alvo.

```text
cp audit.jsonl middle-cut.jsonl
sed '6d' audit.jsonl > middle-cut.jsonl
python3 verify_log.py middle-cut.jsonl
```

O resultado deve ter este formato:

```text
FAIL line=6 expected_seq=6 got=7
```

Se você remover a verificação da sequência, o verificador falhará um teste depois, porque o registro 7 contém o digest do registro 6, enquanto o verificador tem em mãos o digest do registro 5. Mantenha as duas verificações. A divergência da sequência torna o problema claro para o operador, e a divergência do predecessor mostra a relação criptográfica que foi quebrada.

Não diga que uma cadeia de hashes sempre detecta a exclusão no meio. Ela detecta uma exclusão simples quando o invasor não consegue reescrever a cadeia restante. Qualquer pessoa pode calcular novos hashes usando um algoritmo público de digest. Se alguém puder excluir o registro 6, alterar o campo predecessor do registro 7, recalcular o hash do registro 7 e repetir o processo até o registro 12, o arquivo reconstruído poderá ser validado contra seu próprio head novo.

Isso não é um ataque de colisão nem uma quebra do SHA-256. É um recálculo comum. O sistema aceitou um novo histórico porque não tinha evidência protegida de que o histórico antigo existia.

As orientações do NIST sobre evidências digitais deixam o ponto operacional claro: um hash armazenado precisa ficar em um local onde as pessoas com acesso à evidência não possam alterá-lo ou sobrescrevê-lo. O mesmo relatório cita cadeias de hashes como úteis para proteger hashes, mas o armazenamento externo do valor de referência continua sendo necessário.

Por isso, um teste de exclusão no meio tem duas versões:

1. Excluir uma linha e deixar os bytes posteriores intactos. A cadeia deve falhar.
2. Excluir uma linha e regenerar todos os digests posteriores. A cadeia reconstruída deve falhar contra um checkpoint retido de forma independente.

Se seu plano de testes executa apenas a primeira versão, ele testa dano ao arquivo e adulteração descuidada. Não testa um invasor que pode gravar no armazenamento do log.

## Criptografia e integridade da cadeia respondem a perguntas diferentes

Registros criptografados criam uma divisão de responsabilidades útil. Os operadores podem verificar a cadeia sobre o texto cifrado sem ler segredos, corpos de requisições, saídas de comandos ou outro conteúdo sensível da auditoria. Investigadores com o acesso adequado podem descriptografar depois os registros relevantes. Esse é um projeto sensato quando os próprios dados de auditoria são sensíveis.

Mas bytes criptografados não certificam sua própria origem. Um registro de texto cifrado pode ser excluído. Um novo registro pode ser adicionado por alguém que controle o caminho de criptografia e registro. Se o modo de criptografia não autenticar os dados, às vezes é possível alterar o texto cifrado até transformá-lo em lixo sem que o descriptografador saiba o motivo. Use criptografia autenticada para a confidencialidade e a integridade dos registros, e use a cadeia para vinculá-los em um histórico ordenado.

Mantenha estas verificações separadas ao escrever o projeto e o relatório do incidente:

- A autenticação do registro pergunta se um registro criptografado foi alterado.
- A verificação da cadeia pergunta se cada registro restante segue o predecessor declarado.
- A comparação com um checkpoint pergunta se o histórico observado chega a um head observado anteriormente.
- A captura de eventos pergunta se o sistema chegou a gravar a ação no log.

A última pergunta é a mais incômoda. Nenhum log criptográfico pode provar que um evento foi registrado se um escritor comprometido decidiu não emitir um registro. É possível reduzir essa exposição coletando evidências em limites independentes, como um serviço de rede, um recurso de auditoria do host ou um evento de aprovação humana. Não é possível eliminar a lacuna chamando o log local de somente de acréscimo.

O modelo de auditoria do Sallyport é útil nesse contexto porque seus diários de execuções de agentes e de chamadas individuais vêm de um único log de auditoria criptografado e encadeado por hashes. O `sp audit verify` pode verificar offline a cadeia de texto cifrado sem uma chave do cofre. Assim, um revisor consegue testar a continuidade sem expor as credenciais ou os dados das ações protegidos pelo histórico de auditoria.

## Um checkpoint precisa ser difícil de reescrever, não apenas copiado

Um checkpoint é uma declaração sobre o log em determinado momento. No mínimo, inclua um identificador do log, o número de sequência final, o digest final, o horário de criação e a versão do formato ou algoritmo. Trate-o como um artefato separado, com sua própria custódia.

Este formato pequeno basta para um ambiente de testes:

```json
{
  "log_id": "agent-actions-test-a",
  "sequence": 12,
  "head": "8f...c2",
  "created_at": "2026-07-22T14:30:00Z",
  "format": "jsonl-chain-v1"
}
```

Não confie apenas no campo `created_at`. Um relógio local é útil para ordenar eventos e investigar, mas um invasor que controle a máquina pode controlar esse relógio e o arquivo que contém o horário. A força do checkpoint vem do destino para onde ele foi enviado e de quem pode alterá-lo.

Uma configuração prática tem uma parte escrevendo os registros de auditoria e outra retendo os heads periódicos. Essa segunda parte não precisa de acesso à descriptografia. Ela precisa de informação suficiente para rejeitar um arquivo posterior cujo digest final e cuja sequência não correspondam ao que viu antes.

Para uma ferramenta de desenvolvedor, a parte independente pode ser simples: um artefato protegido de CI, um sistema de atestação de release, uma conta de coletor separada ou uma exportação diária aprovada por outro administrador. Para ações de maior risco, emita o checkpoint imediatamente depois da ação e guarde-o fora da estação de trabalho. A periodicidade deve acompanhar a janela de dano que você aceita. Um checkpoint diário não consegue estabelecer a completude das horas entre o head de ontem e o final ausente de hoje.

Assinar um checkpoint ajuda quando os verificadores precisam saber quem o emitiu. A assinatura vincula os dados do checkpoint à autoridade que assina, mas não torna essa autoridade honesta. Se o mesmo processo comprometido escreve o log de auditoria e assina checkpoints substitutos, você ainda tem um único limite de confiança. Coloque a testemunha em um local que o escritor original não possa controlar silenciosamente.

## O armazenamento protegido contra gravação muda o invasor que você está testando

Uma cadeia armazenada em um local onde o escritor pode editar livremente registros antigos tem um modelo de ameaça diferente de uma cadeia armazenada por meio de um caminho de acréscimo protegido contra gravação. O primeiro projeto precisa de testemunhas externas para detectar reescritas do histórico. O segundo tenta impedir que o escritor faça essas reescritas desde o início.

Seja preciso com a expressão «protegido contra gravação». Ela deve significar que o componente que envia um novo registro não pode ler ou alterar livremente registros criptografados anteriores. Isso não significa que o disco não possa falhar, que um administrador não possa excluir um arquivo ou que o sistema operacional não possa ser comprometido. O conceito limita um caminho de ataque: reescrever uma parte escolhida do histórico depois de examiná-la.

Essa diferença afeta seus casos de teste. Para acesso normal a arquivos, teste exclusão seguida de recálculo, porque um invasor pode ler os dados e calcular hashes. Para um armazenamento protegido contra gravação, teste se o chamador pode solicitar exclusão, substituição, rollback ou um segundo log usando a mesma identidade. Teste também como o verificador identifica o log pretendido. Uma cadeia perfeita do arquivo errado continua sendo a evidência errada.

O erro mais comum aqui é tratar permissões de armazenamento como uma âncora criptográfica. Permissões podem ser alteradas por um invasor com privilégios suficientes. Elas continuam importantes porque reduzem danos casuais e limitam os processos que podem agir, mas não substituem um checkpoint que tenha atravessado um limite fora do controle do invasor.

## Teste o rollback separadamente do truncamento

Rollback parece um truncamento do final com um arquivo antigo plausível. Uma máquina pode restaurar o arquivo de auditoria válido de ontem depois de uma falha, de uma restauração de backup ou de uma substituição deliberada. O arquivo restaurado pode passar pelo verificador interno porque realmente era válido ontem.

Seu checkpoint detecta isso se registrar um head mais novo. Compare o arquivo candidato com o checkpoint mais recente retido, não com o mais antigo que você encontrar. Um verificador de cadeia não consegue inferir qual versão válida deveria ser a atual.

Teste diretamente:

1. Salve `audit.jsonl` e seu checkpoint de 12 registros.
2. Gere outro arquivo de teste com mais registros e retenha seu checkpoint mais novo.
3. Substitua o arquivo mais novo pela cópia de 12 registros.
4. Verifique internamente o arquivo restaurado e depois compare-o com o checkpoint mais novo.

Espere dois resultados diferentes. A verificação interna deve ter sucesso. A comparação com o checkpoint deve falhar, porque o arquivo termina em uma sequência e um digest mais antigos. Se as duas verificações falharem, seu artefato pode ter alterado bytes em vez de reproduzir um rollback. Se as duas tiverem sucesso, você está comparando com o checkpoint errado ou não reteve nenhum.

Não «corrija» um rollback anexando registros novos ao arquivo antigo e continuando. Preserve primeiro o estado. Depois de anexar, você terá misturado um histórico possivelmente incompleto com atividades posteriores ao incidente. Comece um novo segmento de log com um limite de incidente registrado ou siga o procedimento de retenção e recuperação já aprovado pela organização.

## Torne os resultados do verificador úteis durante uma investigação

Um verificador que retorna apenas `invalid` cria trabalho desnecessário. Ele deve identificar a primeira condição que falhou sem imprimir payloads sensíveis dos registros. Número de sequência, deslocamento em bytes ou número da linha, digest esperado do predecessor, digest observado do predecessor e digest calculado do registro geralmente fornecem detalhes suficientes.

Evite registrar conteúdo descriptografado em uma mensagem de erro. Uma ferramenta de verificação costuma ser executada em CI, em um pacote de suporte ou em uma captura de terminal. Se um projeto de auditoria criptografada vazar texto claro sempre que a verificação falhar, ele transformará um incidente de integridade em uma exposição de dados.

Uma resposta disciplinada a uma verificação malsucedida segue uma ordem curta:

- Preserve o arquivo original e registre um digest dessa cópia preservada.
- Reúna o checkpoint mais recente, os checkpoints anteriores e quaisquer exportações externas.
- Execute a verificação em cópias e salve a saída exata do comando.
- Compare o head informado com todos os checkpoints retidos.
- Determine se a falha é corrupção, uma lacuna no meio, perda no final, rollback ou uma cadeia reescrita que entra em conflito com uma testemunha.

A classificação importa. Uma falha por lacuna no meio diz que o arquivo contém uma contradição interna. Uma cadeia válida, porém mais curta, diz que o arquivo pode estar completo apenas até seu último registro. Uma cadeia válida que discorda de um checkpoint posterior fornece evidência de rollback, truncamento ou substituição. Uma cadeia reescrita pode parecer limpa internamente, mas discordará de um head independente mais antigo.

Não prometa mais do que as evidências sustentam. «A cadeia de auditoria foi verificada até a sequência 90 e não corresponde ao checkpoint retido da sequência 100» é uma afirmação forte e específica. «Ninguém alterou o log» não é.

## O teste que importa é o que usa o seu limite real

Execute primeiro os testes descartáveis de exclusão de linhas, porque eles ensinam a mecânica. Depois execute os mesmos casos contra a exportação de auditoria real e seu verificador real, usando cópias e os limites de manipulação documentados do formato. Não edite um log criptografado de produção no local apenas para ver o que acontece.

No caso do Sallyport, preserve o log criptografado original e use `sp audit verify` em uma cópia antes e depois de cada alteração controlada. Registre se o verificador detecta uma lacuna interna, se uma cópia encurtada continua sendo verificada com sucesso e se o head retido revela o histórico mais curto. A resposta às três perguntas é mais útil do que uma afirmação vaga de que o log fornece evidência de adulteração.

Uma cadeia válida significa que os registros verificados concordam entre si. Uma cadeia válida mais um checkpoint posterior confiável significa que os registros chegam a um ponto conhecido do histórico. Crie e teste o sistema para a segunda afirmação quando a ausência de ações for relevante.
