← voltar ao site · Relatório publicado como o revisor entregou, inclusive onde aponta erros da auditoria. As correções já foram aplicadas no site.
Revisão independente da auditoria do 2º turno (25/10/2026)
Revisor: agente independente, 26/10/2026. Código próprio em revisao/ (rber.py, bu_decode.py, cmp_all.py, mun_cmp.py, rdv_decode.py, crypto_test.py, forge_test.py, fraud_sim.py, cmp_bu1.py). Não apaguei nem alterei nada fora de revisao/. Fiz cerca de 75 requisições ao TSE, todas registradas nos scripts refetch.py, rdv_fetch.py e crypto_test.py.
Escala de gravidade usada aqui:
- ALTA: muda uma conclusão publicada.
- MÉDIA: uma afirmação vai além do que o código prova.
- BAIXA: imprecisão numérica ou de texto.
---
1. Decodificação do BU (presidente, 2º turno)
O que fiz. Escrevi um parser BER do zero (rber.py) que valida os comprimentos e rejeita comprimento indefinido. O decodificador (bu_decode.py) navega pela tag, não pela posição. Ele soma todos os ResultadoVotacao, exige que partido == código nos votos nominais e conta tipos de voto desconhecidos.
Em vez de rodar só 30 BUs, rodei os 499.178 BUs brutos do 2º turno.
Resultado.
- 499.178 de 499.178 são idênticos a
bu/res.jsonl, em todos os cargos e campos (inclusive governador e a consulta6288-26de MT). - Em todos os BUs:
- há um único
ResultadoVotacaopor eleição; - a soma dos votos de presidente é igual ao comparecimento;
- não existe caso com comparecimento maior que o número de aptos;
- não aparece nenhum candidato além de 13 e 22;
- não há anomalia de partido/código.
- Rebaixei 15 BUs aleatórios do TSE. Os 15 são byte a byte iguais aos de
bu/raw. Os 3 BUs baixados no teste de criptografia também são iguais aos locais. - 1º turno: decodifiquei 3.000 BUs aleatórios de
bu1/raw(presidente6257-1). Os 3.000 são iguais abu1/res.jsonl.
Erros: nenhum no resultado.
Observação (baixa). bu_all.votes() é frágil: usa índices fixos (k[1], k[4], kids[2]), lê a primeira OCTET STRING com mais de 300 bytes e sobrescreveria out[...] se houvesse mais de um ResultadoVotacao. Com o formato de 2026 isso não causa erro, como a conferência completa acima mostra.
Observação (baixa). O BU tem dois campos de aptos (ex.: 308 e 307). O parser usa o primeiro.
2. Soma por município × tse/mun2
O que fiz. Somei a minha própria decodificação de todos os BUs por município (mun_cmp.py). Comparei com vap dos candidatos 13 e 22 e com vb, tvn e e.c de cada arquivo tse/mun2/*-6258.json.
Resultado.
- 5.730 de 5.757 municípios são idênticos. Os 27 que divergem são todos do exterior (
zz), e cada um tem exatamente 1 seção marcadanobu. - Nenhum município sem seção faltante diverge.
- A diferença nacional é de 445 votos de Lula, 353 de Flávio, 12 brancos, 21 nulos e 831 de comparecimento.
- O total nacional oficial (
br-6258b.json) é igual à soma dos municípios: 64.256.232 / 59.826.041 / 1.430.496 / 2.723.305 / 128.236.074. - A afirmação "idêntico exceto onde há seção sem BU" está confirmada.
Erro encontrado (BAIXA-MÉDIA): as 29 seções "sem BU" têm boletim publicado.
- Das 70 seções
nobu, 41 são "Não instalada" e 29 estão "Totalizada". - O
aux.jsondessas 29 lista um boletim do tipobusa(BU do Sistema de Apuração), além derdv,sa.vscelogsa. bu_all.pysó procuratp=='bu', por isso as classificou como seções sem BU.- Baixei 2 desses arquivos
busae decodifiquei: - zz-29297: 13=29, 22=8, nulo=2, comp=39;
- zz-29580: 13=3, 22=28, comp=31.
- Os dois batem exatamente com o oficial.
- A lacuna de 831 votos pode ser fechada baixando 29 arquivos. Essas seções também ficaram de fora da conferência RDV×BU.
3. RDV × BU
Contagem de bu/rdv.jsonl.
- 514.366 linhas, 499.178 ids distintos (iguais ao conjunto de BUs
ok). - Último status por id: 499.177
oke 1nordv(ce-14176-0021-0136, Ipu). Nenhum id tevediffem nenhuma linha. - Todo registro final tem
aux. Toda seção tem uma única entrada de hash, então não há mistura de versões entre BU e RDV. - A afirmação está confirmada.
Lógica do código (rdv_all.py / rdvp.py). Não é comparação de algo consigo mesmo.
- Os votos do RDV são contados voto a voto a partir do RDV baixado.
- Eles são comparados com o BU de
bu/res.jsonl, que vem de outro arquivo. - Se o parser do RDV errasse a chave do cargo, todas as urnas sairiam como
diff. - O mapeamento de tipos é: 1 legenda, 2 nominal, 3 branco, 4/6/7 nulo. Um tipo desconhecido vira
tNe gerariadiff.
Pontos fracos (baixa).
- Só são comparadas as chaves que existem no BU; chaves que só existem no RDV são ignoradas.
- O comparecimento não é comparado diretamente com o número de registros do RDV. Na prática isso fica coberto, porque a soma do BU é igual ao comparecimento.
rdv.jsonlnão guarda as contagens do RDV, só o status. Por isso não dá para reauditar a partir do arquivo sem baixar os RDVs de novo.
Reverificação independente. Baixei 12 RDVs aleatórios e decodifiquei com código próprio (rdv_decode.py). Nas 12, a contagem por tipo e número é igual à do BU. Os nulos são principalmente "00" e números de outros cargos.
Limite de interpretação (MÉDIA, de comunicação). RDV e BU são gerados pelo mesmo software, na mesma urna. A concordância prova consistência interna e ausência de alteração depois da urna. Ela não diz nada sobre um software de urna adulterado.
4. Assinaturas (crypto_check.py, ed521.py)
Revisão do código.
ed521.pyimplementa corretamente a curva E-521 (Edwards a=1, d=−376014, ordem q e base Gy=12 corretos, codificação de 66 bytes com bit de sinal e verificação S·G = R + k·A com SHAKE256-132 e domínioSigEd521).- No caminho "AC URNA", a verificação usa
openssl dgst -sha512 -verify(ECDSA P-521). verify_filescheca:sha512(conteúdo) == hash assinado;- o SHA-512 de cada arquivo aparece na lista assinada;
- a assinatura.
Execução. Rodei o verificador em 3 urnas, uma de cada emissor (AC UE2020 df-97012-0001-0662, AC UE2022 rr-03050-0003-0097, AC URNA mg-41076-0022-0061). As 3 passaram em tudo.
Testes de adulteração (crypto_test.py). Nos 3 emissores:
| Alteração | Resultado do verificador |
|---|---|
| 1 byte no meio do BU | bu=False |
| 1 bit na assinatura | assinatura=False |
| 1 bit no hash assinado | lista=False e assinatura=False |
| 1 bit na lista de hashes | lista=False |
| BU trocado pelo de outra seção | bu=False |
A verificação matemática é real: não passaria sempre.
Erro (MÉDIA): o controle declarado testa pouco. O "controle de adulteração" do código (67 de 67) só inverte 1 byte do BU. Isso testa a busca do hash na lista; não testa o caminho da assinatura. Os meus testes acima cobrem essa lacuna, e o resultado foi bom.
Erro (MÉDIA-ALTA): a afirmação "certificado emitido pelo TSE" é falsa no código, e a falsificação passa.
- O código só compara o texto do CN do emissor com
'AC URNA'. Para os demais emissores, o emissor não é checado: qualquer certificado que não seja "AC URNA" vai para o Ed521. - A assinatura do próprio certificado nunca é verificada, nem existe cadeia até uma raiz.
- Em
forge_test.py, montei uma CA falsa local chamada "AC URNA" e um certificadoCN=ueao01754863com chave própria. Adulterei 1 byte do BU, recalculei a lista de hashes, assinei e reempacotei o.vota. O verificador respondeulista/bu/serial/assinatura = True. - Portanto, o texto do site — "(3) o certificado foi emitido pelo TSE" e "Nenhum boletim, RDV ou log foi alterado ou fabricado fora da urna" — afirma mais do que o teste prova. O teste prova que o arquivo bate com o certificado embutido nele. Não prova que o certificado é do TSE.
- O próprio texto admite: "Não verificamos a cadeia até o certificado raiz".
- A garantia real vem de os arquivos terem sido servidos pelo TSE via HTTPS, e não da criptografia verificada.
Outros pontos (baixa).
- A assinatura de software (
assinaturaSW, primeiro elemento do.vota, cerca de 58 KB) é ignorada. - A amostra "10.000" tem 41 ids repetidos entre as sementes 21 e 77: são 9.959 urnas distintas. O limite estatístico praticamente não muda (3/9.959 ≈ 0,030%).
- O item "limite" do site ainda diz "as assinaturas digitais não foram verificadas criptograficamente", o que contradiz o item "cripto". É texto desatualizado.
5. fraudbound.py, fluxo.py e aritmética nacional
Aritmética (confirmada)
1º turno. 53.879.538 + 56.104.503 + 9.316.747 + 5.975.047 = 125.275.835.
- Os "demais" conferem com a soma dos 10 candidatos em
br-6257.json. - Brancos + nulos inclui 5.246 votos de nulo técnico (
vnt). - A soma dos BUs do 1º turno fica 624 (Lula), 552 (Flávio) e 1.348 (comparecimento) abaixo do oficial, o que é compatível com seções sem
bu. - A categoria "outros" dos BUs fica cerca de 5,1 mil acima do oficial, porque inclui os votos que o TSE contou como nulo técnico. Efeito desprezível.
2º turno. 64.256.232 + 59.826.041 + 4.153.801 = 128.236.074. A diferença é 4.430.191; metade, arredondada, dá 2.215.096, que corresponde a 3,948% dos 56.104.503 votos de Flávio no 1º turno.
Identidade. Ganho de Lula (+10.376.694) + ganho de Flávio (+3.721.538) = 14.098.232. Esse valor é igual a demais (9.316.747) + queda de brancos e nulos (1.821.246) + aumento do comparecimento (+2.960.239).
- Lula ficou com 73,6% desse total líquido.
- O comparecimento subiu 2,4% entre os turnos, bem mais que em 2022 (cerca de +0,5%).
- A conta é consistente, mas é só uma identidade: não testa nada.
Erro (ALTA): o "teto de fraude" de fraudbound.py só vale para um tipo de fraude
O site diz que "uma fraude assim exigiria +3,95%" e que "nada desse tamanho cabe nos dados". O coeficiente Flávio 1T → Lula 2T, num MQO sem intercepto, só sobe 3,95 pontos se a fraude tirar uma fração constante dos votos de Flávio em todas as urnas.
Injetei fraudes sintéticas nos dados reais e refiz o ajuste (fraud_sim.py; urnas pareadas: 496.266):
| Fraude injetada (≈ suficiente para virar) | Votos movidos | Coef. Flávio1T → Lula | Variação |
|---|---|---|---|
| Nenhuma (base) | – | −3,02% | – |
| A) 3,95% dos votos de Flávio → Lula em toda urna | 2,35 mi | +0,93% | +3,95 pp (detectada) |
| B) 2,2 mi Flávio → Lula, proporcional ao voto de Lula | 2,21 mi | −3,12% | −0,10 pp (invisível) |
| C) 2,2 mi Flávio → Lula, só no Nordeste | 2,21 mi | −2,17% | +0,84 pp (abaixo do limiar) |
| D) 4,4 mi votos inventados para Lula a partir da abstenção | 4,43 mi | −2,67% | +0,35 pp (invisível) |
Consequências:
- O modelo está mal especificado mesmo sem fraude: Flávio→Lula sai −3,0% e Lula 1T→Lula 2T sai 101,9%, ambos impossíveis.
- O intervalo de confiança por bootstrap de zonas mede só o ruído amostral, não o viés de especificação. Usá-lo como "teto" não é válido.
- O ponto do site que diz que fraude "apareceria no país inteiro" é uma premissa, não um resultado.
- A fraude do tipo D (votos acrescentados a partir da abstenção) não é limitada por nenhum dos três testes, porque os três medem a perda de Flávio. O padrão observado — Lula cresce onde o comparecimento subiu — não exclui esse tipo, que exigiria cerca de 4,43 mi votos. Excluí-lo exigiria outros dados (biometria e logs), não a regressão.
Os testes 2 e 3 do site ("urnas puras" e Nordeste) limitam razoavelmente desvios de Flávio para Lula, mas não acréscimos. Não reproduzi o código desses dois testes.
Erro (MÉDIA): fluxo.py (mínimos quadrados limitados a [0,1])
- Não há restrição de soma por linha: Zema sai 4% Lula + 100% Flávio = 104%.
- Lula→Lula e Flávio→Flávio ficam presos no limite de 1,0.
- Brancos e nulos do 1º turno saem 81,9% para Lula e 0% para Flávio.
- São artefatos de inferência ecológica e não deveriam ser apresentados como "fluxo de eleitores".
Problemas de texto e amostra (baixa)
- O site diz "499.176 urnas", mas o modelo usa 496.266: o filtro
|aptos1−aptos2| ≤ 2exclui 2.912 urnas sem justificativa.
O que não consegui verificar
- A cadeia de certificados até a raiz do TSE, que não está publicada.
- A assinatura de software (
assinaturaSW). - O conteúdo dos 29 arquivos
busarestantes (baixei 2) e os RDVs dessas seções. - Os testes "urnas puras", forense e Klimek.
- A integridade do software da urna. Nenhum teste feito sobre os arquivos publicados consegue verificar isso.