Pular para o conteúdo
ecdsa.com
← Todos os artigos

Assinaturas de PDF explicadas: ByteRange, CMS, PAdES e LTV

Uma assinatura de PDF assina todos os bytes do arquivo, exceto uma lacuna que guarda a própria assinatura. A maioria dos vereditos confusos vem do que está fora dessa lacuna: revisões posteriores, raízes de confiança ausentes, carimbos do tempo ausentes. Todos os casos abaixo foram assinados e verificados com o pyHanko e o OpenSSL.

Resposta curta. Uma assinatura de PDF é uma assinatura CMS (PKCS #7) guardada na entrada /Contents de um dicionário de assinatura. Ela cobre os bytes listados em /ByteRange, que correspondem ao arquivo inteiro no momento da assinatura, exceto o próprio valor de /Contents. Os validadores respondem a três perguntas distintas. Integridade: os bytes assinados ainda conferem? Confiança: a cadeia de certificados do signatário chega a uma raiz em que o validador confia? Modificações: o que foi anexado depois da assinatura, e isso é permitido? “Válida, mas não confiável”, o ponto de interrogação amarelo do Acrobat, quase sempre significa que os bytes estão intactos, mas a CA do signatário não está na lista de confiança do leitor de PDF. O PAdES (ETSI EN 319 142-1) define níveis de B-B a B-LTA. Um carimbo do tempo RFC 3161 (B-T) prova quando a assinatura existia, e os dados de validação incorporados, junto com os carimbos do tempo de documento (B-LT, B-LTA), a mantêm verificável depois que os certificados expiram.

O que há dentro de um PDF assinado

O teste abaixo assina uma fatura de uma página com o pyHanko 0.38 (CLI 0.6.0), usando uma chave P-256 de uma pequena CA de teste e um carimbo do tempo da TSA pública da DigiCert:

pyhanko sign addsig (pyHanko 0.38.0, 20 de outubro de 2026)
$ pyhanko sign addsig --field Sig1 --use-pades --timestamp-url http://timestamp.digicert.com --reason Approved \
    pemder --key signer.key --cert signer.pem --chain root.pem --no-pass invoice.pdf invoice-signed.pdf

A ferramenta de assinatura não reescreve o arquivo. Ela anexa uma atualização incremental: novos objetos, uma nova seção de referência cruzada e um novo trailer depois do %%EOF original. O arquivo original, de 627 bytes, continua lá, byte a byte, no início do arquivo assinado. A atualização acrescenta um campo de assinatura e um dicionário de assinatura. O dicionário abaixo vem de outra execução do mesmo comando, por isso seus números diferem dos do arquivo examinado a seguir (o valor hexadecimal de /Contents está encurtado):

Um dicionário de assinatura gravado pelo pyHanko (outra execução)
9 0 obj
<<
/Contents <30821CFC06092A864886F70D010702A0821CED3...(zeros)...0000000>
/ByteRange [0 1331 23601 1118]
/Type /Sig
/Filter /Adobe.PPKLite
/SubFilter /ETSI.CAdES.detached
/M (D\07220261020140517Z)
/Reason (Approved)
/Prop_Build <<
/App <<
/Name /pyHanko#20CLI
/REx (0\05638\0560)
>>
>>
>>
endobj

O valor de /M é a data PDF D:20261020140517Z; \072 é um dois-pontos escapado. Um pequeno script que lê /ByteRange e o tamanho de /Contents fornece os números do arquivo usado no restante desta página:

Estrutura da assinatura em invoice-signed.pdf
file size: 24869 bytes, revisions (%%EOF markers): 2
/ByteRange [0 1331 23751 1118]
   signed: bytes 0-1330 and 23751-24868; the 22420-byte gap is the /Contents value
   /Type /Sig  /SubFilter /ETSI.CAdES.detached
   /Contents: 11209 bytes reserved, 7472 bytes used by the CMS object
   range ends at end of file: True

Como as peças se encaixam:

  • /ByteRange [a b c d] lista dois intervalos de bytes: b bytes a partir de a e, em seguida, d bytes a partir de c. A lacuna entre eles é exatamente a string hexadecimal de /Contents, delimitadores incluídos. Todo o resto do arquivo no momento da assinatura entra no cálculo do hash. Como o tamanho do arquivo só é conhecido depois que a assinatura está no lugar, as ferramentas de assinatura gravam um valor provisório, preenchem /ByteRange, calculam o hash, assinam e só então gravam a assinatura no espaço reservado.
  • /Contents guarda o SignedData CMS codificado em DER, convertido em hexadecimal e completado com zeros. O pyHanko reservou 11.209 bytes e usou 7.472. O restante é padding, que existe porque o espaço precisa ser reservado antes de se conhecer o tamanho da assinatura.
  • /SubFilter indica o formato. ETSI.CAdES.detached é o formato PAdES; adbe.pkcs7.detached é o formato mais antigo, da ISO 32000-1. Ambos são assinaturas CMS desanexadas sobre os bytes do ByteRange.
  • /M é o horário de assinatura declarado pelo signatário. O PAdES o coloca aqui: o perfil baseline diz que o atributo CMS signing-time “não deve estar presente” em nenhum nível. É apenas uma declaração, já que qualquer pessoa com a chave pode escrever o horário que quiser.

O objeto CMS não tem nada de especial, e o OpenSSL o verifica com os intervalos extraídos:

openssl cms -verify em /Contents (OpenSSL 3.5.7)
CMS Verification successful
      1 object: contentType (1.2.840.113549.1.9.3)
      1 object: id-smime-aa-signingCertificateV2 (1.2.840.113549.1.9.16.2.47)
      1 object: id-smime-aa-timeStampToken (1.2.840.113549.1.9.16.2.14)
      1 object: messageDigest (1.2.840.113549.1.9.4)

Os atributos assinados são o tipo de conteúdo, o messageDigest dos bytes do ByteRange e o signingCertificateV2, um hash do certificado do signatário que impede alguém de trocá-lo por outro certificado com a mesma chave. O atributo não assinado timeStampToken é o carimbo do tempo RFC 3161 sobre o valor da assinatura. O verificador de assinaturas de PDF mostra esse detalhamento para qualquer PDF, incluindo quais bytes cada assinatura cobre. O inspetor de carimbo do tempo decodifica o token incorporado.

Três perguntas que um validador responde

O relatório do pyHanko mantém as três separadas, o que o torna bem didático. Com a raiz da CA de teste fornecida como confiável:

pyhanko sign validate --pretty-print --trust root.pem invoice-signed.pdf (resumido)
Trust anchor: "Organization: Example GmbH, Common Name: Example Signing Root CA"
The signer's certificate is trusted.

Integrity
---------
The signature is cryptographically sound.

Signing time
------------
Signing time as reported by signer: 2026-10-20T14:04:36+00:00

Signature timestamp token: 2026-10-20T14:04:37+00:00
The token is guaranteed to be newer than the signature.
This timestamp is backed by a time stamping authority.
The timestamp token is cryptographically sound.
TSA certificate subject: "Common Name: DigiCert SHA256 RSA4096 Timestamp Responder 2026 1; Organization: DigiCert, Inc.; Country: US"
The TSA certificate is trusted.

Modifications
-------------
The signature covers the entire file.

Bottom line
-----------
The signature is judged VALID.

Válida, mas não confiável

O mesmo arquivo, validado sem fornecer a raiz de teste:

Mesmo arquivo, sem raiz de confiança para o signatário (resumido)
Trust anchor: "No path to trust anchor found."
The signer's certificate is untrusted.

Integrity
---------
The signature is cryptographically sound.
...
Bottom line
-----------
The signature is judged INVALID.

A integridade está intacta; falta a confiança. É isso que o Acrobat informa como “Signature validity is UNKNOWN”, com uma observação de que a identidade do signatário é desconhecida. É a reclamação mais comum sobre PDFs assinados, e em geral o documento está em ordem. Os leitores de PDF confiam em certificados de assinatura por meio de listas próprias. O Acrobat usa a Adobe Approved Trust List (AATL) e, opcionalmente, as listas de confiança da UE (EU Trusted Lists). É o caso de certificados de uma CA da própria empresa, de um cartão de identidade nacional que o leitor de PDF não conhece ou de um certificado autoassinado. As correções ficam do lado do leitor (confiar na CA emissora ou ativar as listas de confiança da UE) ou do lado do signatário (assinar com um certificado de uma CA em que os destinatários confiam). Observe o veredito separado do certificado da TSA: a cadeia da TSA da DigiCert chega a uma raiz do trust store do sistema, então o carimbo do tempo é confiável mesmo quando o signatário não é.

Bytes alterados dentro do intervalo assinado

Substituindo EUR 1,250.00 por EUR 9,250.00 diretamente no arquivo:

Um valor editado diretamente no arquivo (resumido)
Integrity
---------
The signature is cryptographically unsound.
...
Bottom line
-----------
The signature is judged INVALID.

Não há como contornar este caso. Todo byte dos intervalos entra no cálculo do hash, então qualquer edição feita diretamente no arquivo quebra a assinatura.

Alterações anexadas depois da assinatura

Os ataques interessantes não tocam nos bytes assinados. Eles acrescentam uma nova revisão. O PDF permite atualizações incrementais, e elas são legítimas para preencher formulários, adicionar anotações ou acrescentar mais assinaturas. O teste anexa uma revisão que substitui o conteúdo da página pelo novo valor:

Conteúdo substituído em uma atualização incremental depois da assinatura
file size: 26337 bytes, revisions (%%EOF markers): 3
/ByteRange [0 1331 23751 1118]
   range ends at end of file: False
...
The signature is cryptographically sound.
...
Modifications
-------------
The signature does not cover the entire file.
Some modifications may be illegitimate, and they appear to be incompatible with the current document modification policy.

Bottom line
-----------
The signature is judged INVALID.

A assinatura continua criptograficamente íntegra, porque a revisão assinada não foi tocada. O que o leitor vê é a nova revisão. Só a análise de diferenças detecta isso: ela compara o que mudou depois da assinatura com o que é permitido. Um visualizador que só verifica “assinatura íntegra, signatário confiável” exibe EUR 9.250 sob uma assinatura válida. Pesquisas sobre visualizadores de PDF de 2019 em diante encontraram exatamente essas falhas em muitos aplicativos (ataques de salvamento incremental e shadow attacks). Já acrescentar uma segunda assinatura é uma alteração permitida:

Uma segunda assinatura adicionada depois da primeira: o veredito de Sig1
Modifications
-------------
The signature does not cover the entire file.
All modifications relate to signing and form filling operations, and they appear to be compatible with the current document modification policy.

Bottom line
-----------
The signature is judged VALID.

Uma assinatura de certificação (--certify no pyHanko, “Certificar” no Acrobat) registra nas suas permissões DocMDP a política do autor para alterações posteriores: nenhuma alteração, apenas preenchimento de formulários, ou preenchimento de formulários e anotações. Os validadores a aplicam na análise de diferenças.

Tempo: carimbos do tempo RFC 3161

Uma assinatura é feita com um certificado que expira e pode ser revogado. Sem uma prova independente de quando a assinatura foi feita, um validador só consegue verificar o certificado como ele está agora. Depois que ele expira, ou é revogado por qualquer motivo, a assinatura parece tão ruim quanto uma feita ontem com uma chave roubada. A entrada /M não ajuda, porque é o próprio signatário quem a escolhe.

Um carimbo do tempo RFC 3161 de uma autoridade de carimbo do tempo (TSA) confiável resolve isso. A TSA assina um token que contém o hash do valor da assinatura e o horário da própria TSA. No teste, a TSA da DigiCert devolveu um token com o horário 14:04:37 UTC, um segundo depois do horário de assinatura declarado. Isso prova que a assinatura já existia nesse momento, enquanto o certificado era válido. Esse é o nível B-T do PAdES. Carimbos do tempo também podem ser adicionados como um carimbo do tempo de documento separado: um dicionário de assinatura com /Type /DocTimeStamp e /SubFilter /ETSI.RFC3161 que aplica um carimbo do tempo ao documento inteiro, tal como ele está.

Validação de longo prazo: B-LT e B-LTA

Validar daqui a dez anos exige mais do que um carimbo do tempo. As CRLs ou os respondedores OCSP da CA podem ter deixado de existir, e os algoritmos podem ter se enfraquecido. A norma ETSI EN 319 142-1 (V1.2.1, janeiro de 2024) define os níveis:

NívelAcrescentaFinalidade
B-BA assinatura CMS com os atributos assinados obrigatóriosAssinatura básica
B-TUm carimbo do tempo confiávelProva que a assinatura existia em um dado momento
B-LTTodo o material de validação: certificados, CRLs e respostas OCSP no DSS do documentoValidação sem buscar nada na rede
B-LTACarimbos do tempo de documento sobre o documento, incluindo esse material, renovados ao longo do tempoMantém o material e os carimbos do tempo confiáveis à medida que os certificados expiram e os algoritmos envelhecem

O material de validação fica no Document Security Store (/DSS no catálogo do documento), adicionado em uma atualização incremental depois da assinatura. Assinando a mesma fatura no nível B-LTA, com uma CRL servida pela CA de teste:

pyhanko sign addsig --use-pades-lta --with-validation-info (resumido)
file size: 54674 bytes, revisions (%%EOF markers): 4
/ByteRange [0 1331 23745 1099]
   /Type /Sig  /SubFilter /ETSI.CAdES.detached
   range ends at end of file: False
/ByteRange [0 34471 52463 668]
   /Type /DocTimeStamp  /SubFilter /ETSI.RFC3161
   range ends at end of file: False
catalog /DSS: True
   /Certs present: True
   /CRLs present: True
   /OCSPs present: True
   /VRI present: True
...
Modifications
-------------
The signature does not cover the entire file.
All modifications relate to signature maintenance, and they appear to be compatible with the current document modification policy.

Bottom line
-----------
The signature is judged VALID.

Quatro revisões: a original; a assinatura; uma atualização que acrescenta o DSS com os dados de validação e termina com o carimbo do tempo de documento, cujo intervalo de bytes vai até o byte 53.131; e uma última atualização, de 1.543 bytes, anexada depois do carimbo do tempo e que o validador conta como manutenção da assinatura. Listar o DSS de um arquivo assinado da mesma forma mostra o que “todo o material de validação” significa na prática. Ele contém a cadeia do signatário e a CRL da CA de teste, e também a cadeia do respondedor de carimbo do tempo da DigiCert com duas respostas OCSP, porque o carimbo do tempo também precisa continuar verificável:

Document Security Store de um arquivo B-LTA (lido com pyHanko e asn1crypto)
certs:
   Organization: Example GmbH, Common Name: Example Signing Root CA
   Organization: Example GmbH, Common Name: Alice Example
   Common Name: DigiCert Trusted Root G4, Organizational Unit: www.digicert.com, Organization: DigiCert Inc, Country: US
   Common Name: DigiCert Trusted G4 TimeStamping RSA4096 SHA256 2025 CA1; Organization: DigiCert, Inc.; Country: US
   Common Name: DigiCert SHA256 RSA4096 Timestamp Responder 2026 1; Organization: DigiCert, Inc.; Country: US
CRLs:
   Organization: Example GmbH, Common Name: Example Signing Root CA
OCSP responses:
   ef6f534ae9e4067c7acae29056f62fd449eccb4e
   ecd7e382d2715d644cdf2e673fe7ba98ae1c0f4f

Os respondedores OCSP são identificados pelo hash da chave. O carimbo do tempo de documento, por sua vez, precisa ser renovado antes que o certificado da TSA dele expire, e pyhanko sign ltaupdate acrescenta um novo. O Acrobat rotula como “LTV enabled” (habilitada para LTV) as assinaturas cujo material de validação está incorporado.

Que nível produzir

  • Use sempre carimbo do tempo. O B-B basta apenas para documentos lidos e arquivados dentro do prazo de validade do certificado, e mesmo nesse caso um carimbo do tempo custa uma única requisição HTTP a uma TSA. Adote o B-T como mínimo.
  • Use o B-LTA para tudo o que for arquivado. Contratos, registros regulados e faturas que precisam continuar verificáveis anos depois exigem dados de validação incorporados e carimbos do tempo de documento. Agende as execuções de ltaupdate bem antes de expirar o certificado da TSA do carimbo do tempo de documento mais recente, e antes que os algoritmos dele passem a ser considerados fracos.
  • O efeito jurídico é outra questão. Pelo Regulamento eIDAS da UE, o Regulamento (UE) nº 910/2014, uma assinatura eletrônica qualificada exige um certificado qualificado de um prestador de serviços de confiança que conste das listas de confiança da UE, além de um dispositivo qualificado de criação de assinaturas. O PAdES descreve o formato do arquivo, não esse nível jurídico. As opções --eutl-territories e --require-qualified do pyHanko verificam o lado das listas de confiança.
  • Assine com uma CA em que os leitores confiam. Para destinatários externos, isso significa uma CA da AATL ou das listas de confiança da UE. Uma CA interna serve quando todos os leitores podem ser configurados para confiar nela.

Como verificar um PDF assinado

  1. Examine todas as assinaturas e revisões, e não só a faixa verde. Quantas revisões vêm depois da última assinatura, e o que elas alteram?
  2. Separe os vereditos. Integridade falhou → os bytes assinados mudaram. Confiança falhou → uma questão de confiança do lado do leitor. Modificações sinalizadas → algum conteúdo foi adicionado depois da assinatura, e você precisa ver qual.
  3. Verifique as evidências de horário. Há um carimbo do tempo de uma TSA em que você confia, ou só o /M do signatário?
  4. Para arquivamento, verifique o LTV. O material de validação está incorporado, e o carimbo do tempo de documento mais recente ainda está dentro da validade do certificado da TSA dele?
  5. Use um validador que informe tudo isso. Em scripts, pyhanko sign validate --pretty-print com um --trust explícito, ou o pyhanko sign adesverify, no estilo AdES. O pyHanko 0.38 avisa que recorrer ao trust store do sistema operacional “is deprecated and will be removed in a future release” (está obsoleto e será removido em uma versão futura), então passe suas raízes de confiança explicitamente. No navegador, o verificador de assinaturas de PDF faz o mesmo sem enviar o documento.

Erros comuns

  • Confiar na imagem visível da assinatura. O carimbo desenhado na página é decoração. A assinatura é o objeto CMS, e uma imagem não prova nada.
  • Tratar “assinatura válida” como “documento inalterado”. Só “covers the entire file” (a assinatura cobre o arquivo inteiro) ou uma análise de diferenças sem ressalvas significam isso.
  • Adicionar o próprio certificado do signatário como confiável para fazer um aviso sumir. Confie na CA emissora, depois de verificá-la, ou simplesmente não confie.
  • Sem carimbo do tempo. Sem o carimbo do tempo de uma TSA, a evidência da assinatura acaba quando o certificado expira.
  • Salvar de novo um PDF assinado em uma ferramenta que reescreve o arquivo. Muitos editores fazem um salvamento completo em vez de um incremental, o que quebra todas as assinaturas existentes.

Fontes

  • ISO 32000-2 (PDF 2.0), §12.8 (assinaturas digitais). A ISO 32000-1 define adbe.pkcs7.detached e o dicionário de assinatura.
  • ETSI EN 319 142-1 V1.2.1 (2024-01): blocos de construção e níveis baseline do PAdES, carimbos do tempo de documento.
  • RFC 5652 (CMS), RFC 3161 (carimbos do tempo), RFC 5035 (signingCertificateV2).
  • Documentação e CLI do pyHanko 0.38.0.
  • PDF Insecurity: pesquisa sobre ataques de salvamento incremental (incremental saving), de encapsulamento de assinaturas (wrapping) e shadow attacks.

Todos os arquivos desta página foram criados em 20 de outubro de 2026 no node:24.22.0-trixie, com o pyHanko 0.38.0 (pyhanko-cli 0.6.0, pyhanko-certvalidator 0.33.0) em um virtualenv e o OpenSSL 3.5.7, com carimbos do tempo de http://timestamp.digicert.com.