Programação Por Equipe DevBras

Capa do artigo: Conferir assinatura de PDF: ITI, pdfsig e pyHanko

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.

Avalie este artigo

Referência: https://assina.ufsc.br/verificador/