Conferir assinatura de PDF: ITI, pdfsig e pyHanko
Assinatura de PDF válida não quer dizer arquivo intacto: veja o que o pdfsig e o pyHanko mostram, faça uma triagem automática e use o validador do ITI.
Um teste rápido mostra o problema. Pegue um contrato assinado digitalmente, acrescente uma anotação "Valor mensal: R$ 9.200,00" por cima da cláusula de R$ 3.200,00 e passe o arquivo no pdfsig. A resposta inclui a linha Signature Validation: Signature is Valid.. A assinatura continua válida porque os bytes que ela cobre não mudaram. O que mudou veio depois, num trecho que ela não cobre.
Quem recebe o PDF, seja o financeiro que libera pagamento ou o sistema que aceita documentos de clientes, precisa ler a conferência inteira, não só a palavra "válida". Este guia mostra o que cada resultado quer dizer, com três versões do mesmo contrato de teste, e onde fazer a conferência oficial no Brasil.
O que "assinatura válida" garante, e o que não
Uma assinatura digital em PDF prova duas coisas: que um trecho de bytes não foi alterado desde a assinatura e que quem assinou tinha a chave privada do certificado. Ela não diz, sozinha:
- se o certificado merece confiança, ou seja, se foi emitido por uma autoridade reconhecida e não por qualquer um com OpenSSL;
- se o titular é quem você esperava: um certificado com nome parecido passa na conta criptográfica;
- se o arquivo tem algo além do que foi assinado. O PDF permite atualização incremental: conteúdo novo é anexado no fim do arquivo, sem tocar nos bytes antigos. Formulário preenchido e segunda assinatura usam esse recurso de forma legítima, e uma anotação maliciosa usa do mesmo jeito.
Três arquivos, três respostas
Para ver cada caso, o contrato de teste nº 2026-118 foi assinado com pyHanko e um certificado autoassinado (o passo a passo está no guia para assinar PDF com Python e certificado A1). Depois, o acréscimo foi feito com o próprio pyHanko, em poucas linhas:
from pyhanko.pdf_utils.incremental_writer import IncrementalPdfFileWriter
from pyhanko.pdf_utils import generic
with open("contrato-assinado.pdf","rb") as f:
w = IncrementalPdfFileWriter(f)
# acrescenta uma anotação de texto livre na página 1, depois da assinatura
page = w.root['/Pages']['/Kids'][0].get_object()
annot = generic.DictionaryObject({
generic.NameObject('/Type'): generic.NameObject('/Annot'),
generic.NameObject('/Subtype'): generic.NameObject('/FreeText'),
generic.NameObject('/Rect'): generic.ArrayObject([generic.FloatObject(x) for x in (72,600,400,630)]),
generic.NameObject('/Contents'): generic.pdf_string('Valor mensal: R$ 9.200,00'),
generic.NameObject('/DA'): generic.pdf_string('/Helv 12 Tf 0 g'),
})
ref = w.add_object(annot)
page[generic.NameObject('/Annots')] = generic.ArrayObject([ref])
w.update_container(page)
with open("contrato-com-anotacao.pdf","wb") as out: w.write(out)
A terceira versão teve um único byte trocado dentro do trecho assinado. As três passaram pelo pdfsig e pelo pyhanko sign validate com o certificado de teste marcado como confiável:
- Intacto:
Total document signed,Signature is Valid.; pyHanko: VALID. - Acrescentado depois:
Not total document signed,Signature is Valid.; pyHanko: "The signature does not cover the entire file" e INVALID. - Byte trocado:
Total document signed,Digest Mismatch.; pyHanko: INVALID.
O caso do meio é o perigoso. O pdfsig responde as duas perguntas separadamente, e quem lê só a linha da validação aprova o contrato com o valor trocado. O pyHanko vai além: ele examina o que foi acrescentado e, quando a modificação não se encaixa na política de alterações aceitas, avisa que ela pode ser ilegítima e julga a assinatura inválida. A documentação de validação do pyHanko descreve as opções.

Triagem automática na entrada
Num sistema que recebe contratos, dá para separar os casos antes de alguém abrir o arquivo. O script abaixo usa só o pdfsig (manual aqui) e a ordem das perguntas importa: primeiro se existe assinatura, depois se cobre o arquivo todo, por fim se confere:
#!/bin/sh
# conferir.sh: triagem automática de PDF assinado recebido
for pdf in "$@"; do
saida=$(pdfsig "$pdf" 2>/dev/null)
if ! printf '%s\n' "$saida" | grep -q "Signature #"; then
echo "SEM ASSINATURA $pdf"
elif printf '%s\n' "$saida" | grep -q "Not total document signed"; then
echo "ALTERADO DEPOIS $pdf"
elif ! printf '%s\n' "$saida" | grep -q "Signature is Valid"; then
echo "INVALIDA $pdf"
else
echo "INTEGRO $pdf ($(printf '%s\n' "$saida" | grep -m1 'Common Name' | cut -d: -f2- | sed 's/^ //'))"
fi
done
SEM ASSINATURA contrato.pdf
INTEGRO contrato-assinado.pdf (Loja Teste Ltda)
ALTERADO DEPOIS contrato-com-anotacao.pdf
INVALIDA contrato-adulterado.pdf
Limites conhecidos: com duas assinaturas, a primeira aparece como Not total document signed porque a segunda veio depois, e o script manda o arquivo para revisão manual. E "INTEGRO" não quer dizer confiável. O certificado de teste passaria aqui; a confiança vem do passo seguinte.
A conferência oficial: validar.iti.gov.br
O Validador de Assinaturas do ITI é o serviço oficial do governo para assinaturas ICP-Brasil, GOV.BR e de acordos internacionais de reconhecimento mútuo. O ITI informa que não armazena os arquivos enviados e que o resultado se limita a identificar o titular do certificado e confirmar que o documento não foi adulterado depois da assinatura.
Essa última frase define o que sobra para você conferiUm teste rápido mostra o problema. Pegue um contrato assinado digitalmente, acrescente uma anotação "Valor mensal: R$ 9.200,00" por cima da cláusula de R$ 3.200,00 e passe o arquivo no pdfsig. A resposta inclui a linha Signature Validation: Signature is Valid.. A assinatura continua válida porque os bytes que ela cobre não mudaram. O que mudou veio depois, num trecho que ela não cobre.
Quem recebe o PDF, seja o financeiro que libera pagamento ou o sistema que aceita documentos de clientes, precisa ler a conferência inteira, não só a palavra "válida". Este guia mostra o que cada resultado quer dizer, com três versões do mesmo contrato de teste, e onde fazer a conferência oficial no Brasil.
O que "assinatura válida" garante, e o que não
Uma assinatura digital em PDF prova duas coisas: que um trecho de bytes não foi alterado desde a assinatura e que quem assinou tinha a chave privada do certificado. Ela não diz, sozinha:
- se o certificado merece confiança, ou seja, se foi emitido por uma autoridade reconhecida e não por qualquer um com OpenSSL;
- se o titular é quem você esperava: um certificado com nome parecido passa na conta criptográfica;
- se o arquivo tem algo além do que foi assinado. O PDF permite atualização incremental: conteúdo novo é anexado no fim do arquivo, sem tocar nos bytes antigos. Formulário preenchido e segunda assinatura usam esse recurso de forma legítima, e uma anotação maliciosa usa do mesmo jeito.
Três arquivos, três respostas
Para ver cada caso, o contrato de teste nº 2026-118 foi assinado com pyHanko e um certificado autoassinado (o passo a passo está no guia para assinar PDF com Python e certificado A1). Depois, o acréscimo foi feito com o próprio pyHanko, em poucas linhas:
from pyhanko.pdf_utils.incremental_writer import IncrementalPdfFileWriter
from pyhanko.pdf_utils import generic
with open("contrato-assinado.pdf","rb") as f:
w = IncrementalPdfFileWriter(f)
# acrescenta uma anotação de texto livre na página 1, depois da assinatura
page = w.root['/Pages']['/Kids'][0].get_object()
annot = generic.DictionaryObject({
generic.NameObject('/Type'): generic.NameObject('/Annot'),
generic.NameObject('/Subtype'): generic.NameObject('/FreeText'),
generic.NameObject('/Rect'): generic.ArrayObject([generic.FloatObject(x) for x in (72,600,400,630)]),
generic.NameObject('/Contents'): generic.pdf_string('Valor mensal: R$ 9.200,00'),
generic.NameObject('/DA'): generic.pdf_string('/Helv 12 Tf 0 g'),
})
ref = w.add_object(annot)
page[generic.NameObject('/Annots')] = generic.ArrayObject([ref])
w.update_container(page)
with open("contrato-com-anotacao.pdf","wb") as out: w.write(out)
A terceira versão teve um único byte trocado dentro do trecho assinado. As três passaram pelo pdfsig e pelo pyhanko sign validate com o certificado de teste marcado como confiável:
- Intacto:
Total document signed,Signature is Valid.; pyHanko: VALID. - Acrescentado depois:
Not total document signed,Signature is Valid.; pyHanko: "The signature does not cover the entire file" e INVALID. - Byte trocado:
Total document signed,Digest Mismatch.; pyHanko: INVALID.
O caso do meio é o perigoso. O pdfsig responde as duas perguntas separadamente, e quem lê só a linha da validação aprova o contrato com o valor trocado. O pyHanko vai além: ele examina o que foi acrescentado e, quando a modificação não se encaixa na política de alterações aceitas, avisa que ela pode ser ilegítima e julga a assinatura inválida. A documentação de validação do pyHanko descreve as opções.
Triagem automática na entrada
Num sistema que recebe contratos, dá para separar os casos antes de alguém abrir o arquivo. O script abaixo usa só o pdfsig (manual aqui) e a ordem das perguntas importa: primeiro se existe assinatura, depois se cobre o arquivo todo, por fim se confere:
#!/bin/sh
# conferir.sh: triagem automática de PDF assinado recebido
for pdf in "$@"; do
saida=$(pdfsig "$pdf" 2>/dev/null)
if ! printf '%s\n' "$saida" | grep -q "Signature #"; then
echo "SEM ASSINATURA $pdf"
elif printf '%s\n' "$saida" | grep -q "Not total document signed"; then
echo "ALTERADO DEPOIS $pdf"
elif ! printf '%s\n' "$saida" | grep -q "Signature is Valid"; then
echo "INVALIDA $pdf"
else
echo "INTEGRO $pdf ($(printf '%s\n' "$saida" | grep -m1 'Common Name' | cut -d: -f2- | sed 's/^ //'))"
fi
done
SEM ASSINATURA contrato.pdf
INTEGRO contrato-assinado.pdf (Loja Teste Ltda)
ALTERADO DEPOIS contrato-com-anotacao.pdf
INVALIDA contrato-adulterado.pdf
Limites conhecidos: com duas assinaturas, a primeira aparece como Not total document signed porque a segunda veio depois, e o script manda o arquivo para revisão manual. E "INTEGRO" não quer dizer confiável. O certificado de teste passaria aqui; a confiança vem do passo seguinte.
A conferência oficial: validar.iti.gov.br
O Validador de Assinaturas do ITI é o serviço oficial do governo para assinaturas ICP-Brasil, GOV.BR e de acordos internacionais de reconhecimento mútuo. O ITI informa que não armazena os arquivos enviados e que o resultado se limita a identificar o titular do certificado e confirmar que o documento não foi adulterado depois da assinatura.
Essa última frase define o que sobra para você conferir no relatório:
- O titular. Compare o titular do certificado mostrado no relatório com o cadastro de quem deveria assinar, não com o nome escrito no carimbo visível da página, que é só desenho.
- A data. Com carimbo do tempo, a data vem de uma terceira parte; sem ele, é a data que o computador de quem assinou informou.
- O conteúdo. O validador confirma que o arquivo não mudou. Se o que foi assinado já tinha o valor errado, a assinatura continua válida.
Que nível de assinatura a situação pede
A pergunta "essa assinatura vale?" tem resposta jurídica, não técnica. Para interações com órgãos públicos, a Lei 14.063/2020 define três níveis, resumidos pelo Governo Digital:
- simples: identifica quem assina e associa os dados ao documento;
- avançada: usa certificado não emitido pela ICP-Brasil ou outro meio de comprovar autoria e integridade, desde que aceito pelas partes. É o caso da assinatura GOV.BR;
- qualificada: usa certificado digital ICP-Brasil, nos termos da MP 2.200-2/2001.
Entre empresas, quem decide o nível aceito é o contrato e o risco do negócio. O sistema só consegue impor o que foi decidido: aceitar apenas certificado ICP-Brasil, exigir carimbo do tempo, recusar arquivo alterado depois. Se o seu sistema também assina documentos, a chave usada ali merece o mesmo rigor, assunto do post sobre assinar PDF sem entregar a chave.r no relatório:
- O titular. Compare o titular do certificado mostrado no relatório com o cadastro de quem deveria assinar, não com o nome escrito no carimbo visível da página, que é só desenho.
- A data. Com carimbo do tempo, a data vem de uma terceira parte; sem ele, é a data que o computador de quem assinou informou.
- O conteúdo. O validador confirma que o arquivo não mudou. Se o que foi assinado já tinha o valor errado, a assinatura continua válida.
Que nível de assinatura a situação pede
A pergunta "essa assinatura vale?" tem resposta jurídica, não técnica. Para interações com órgãos públicos, a Lei 14.063/2020 define três níveis, resumidos pelo Governo Digital:
- simples: identifica quem assina e associa os dados ao documento;
- avançada: usa certificado não emitido pela ICP-Brasil ou outro meio de comprovar autoria e integridade, desde que aceito pelas partes. É o caso da assinatura GOV.BR;
- qualificada: usa certificado digital ICP-Brasil, nos termos da MP 2.200-2/2001.
Entre empresas, quem decide o nível aceito é o contrato e o risco do negócio. O sistema só consegue impor o que foi decidido: aceitar apenas certificado ICP-Brasil, exigir carimbo do tempo, recusar arquivo alterado depois. Se o seu sistema também assina documentos, a chave usada ali merece o mesmo rigor, assunto do post sobre assinar PDF sem entregar a chave.
Referência: https://assina.ufsc.br/verificador/