Pular para o conteúdo

Como as assinaturas se encaixam em um PDF

Spec: ETSI EN 319 142-1 Spec: RFC 5652 Evidence: Standard-backed

Uma assinatura de PDF não fica em volta do arquivo. Ela é embutida dentro dele: um dicionário que identifica a assinatura e um digest calculado sobre um intervalo declarado de bytes, ignorando deliberadamente o próprio valor da assinatura. Esta página explica esse mecanismo e, com a mesma importância, o que ele não promete.

“O documento está assinado” é uma frase que as pessoas usam para tomar decisões. Elas a associam a um pagamento, uma aprovação ou uma obrigação legal. Se você não sabe exatamente quais bytes uma assinatura cobre, não consegue dizer o que um resultado válido realmente prova. Um PDF pode conter uma assinatura perfeitamente válida e, ainda assim, exibir ao leitor conteúdo que o signatário nunca viu, porque esse conteúdo foi adicionado depois da assinatura, em uma região que ela nunca reivindicou. Saber onde a autoridade da assinatura começa e termina é a diferença entre uma decisão defensável e uma decisão baseada em suposição.

  • Uma assinatura de PDF reside em um dicionário de assinatura e em um campo de assinatura dentro do documento, não como um envelope externo.
  • Os bytes assinados são declarados por um array ByteRange: dois segmentos (offset, length) que, juntos, cobrem o arquivo inteiro exceto o valor hexadecimal da assinatura contido na entrada Contents.
  • O digest desses dois segmentos concatenados é o que a assinatura criptográfica realmente protege.
  • Tudo o que for anexado posteriormente em uma nova revisão fica fora do byte range original. A assinatura original permanece válida; ela nunca fez nenhuma afirmação sobre os novos bytes.
  • Uma assinatura de aprovação e uma assinatura de certificação diferem em escopo: a certificação (DocMDP) restringe quais alterações posteriores são permitidas; a aprovação não.

O NextPDF constrói a assinatura de acordo com o modelo do formato, em uma ordem fixa, para que o byte range seja exato, não aproximado.

Quando o engine grava uma assinatura, primeiro reserva um espaço de tamanho fixo para o valor Contents e grava um placeholder ByteRange de largura fixa. Ele aguarda até que o documento completo seja gravado, incluindo a tabela de referência cruzada e o marcador de fim de arquivo. Só então calcula os dois offsets reais, grava-os de volta no placeholder sem deslocar nenhum byte, aplica hash aos dois segmentos e coloca o objeto CMS resultante no espaço reservado. O placeholder é preenchido com zeros até um comprimento constante justamente para que inserir os números reais não desloque os bytes que serão submetidos ao hash. Essa é a única ordem que produz uma assinatura autoconsistente. O engine trata qualquer falha nessa sequência como erro fatal, não como fallback silencioso.

Para o perfil PDF 2.0, o próprio objeto de assinatura é uma estrutura CMS SignedData destacada (detached). O dicionário do PDF diz onde e como; o objeto CMS carrega o quem e a prova criptográfica.

  1. Step 1 of 4: ISO 32000-2 §12.8.1 — ByteRange digest & signature dictionary
  2. Step 2 of 4: ISO 32000-2 §12.8.3.3 — ETSI.CAdES.detached SubFilter
  3. Step 3 of 4: ETSI EN 319 142-1 PAdES baseline profile
  4. Step 4 of 4: RFC 5652 CMS SignedData in Contents
Onde uma assinatura de PDF é definida, do formato de contêiner até o objeto criptográfico: a ISO 32000-2 especifica o dicionário e o mecanismo de byte range, a ETSI EN 319 142-1 o perfila para PAdES e a RFC 5652 define o objeto CMS SignedData colocado em Contents.

Evidence: Standard-backed O mecanismo é definido pela Spec: ISO 32000-2, §12.8.1 . Um digest de byte range é calculado sobre um intervalo de bytes indicado pela entrada ByteRange. Esse intervalo deve ser o arquivo inteiro incluindo o dicionário de assinatura, mas excluindo o valor da assinatura — a entrada Contents. ByteRange é um array de pares de inteiros: offset inicial e comprimento. Intervalos descontíguos são usados especificamente para permitir que o digest omita o próprio valor da assinatura.

Para o perfil PDF 2.0, a Spec: ISO 32000-2, §12.8.3.3 especifica que, quando o SubFilter é ETSI.CAdES.detached, o valor de Contents é um objeto CMS SignedData codificado em DER — a mesma estrutura definida pela Spec: RFC 5652 — e o perfil PAdES desse objeto é o que a Spec: ETSI EN 319 142-1 descreve.

O escopo não é igual para todas as assinaturas. A Spec: ISO 32000-2, §12.7.4.5 define a permissão MDP: um valor de 0 torna a assinatura uma assinatura de aprovação, enquanto os valores 13 a tornam uma assinatura de certificação que restringe quais modificações posteriores mantêm o documento em conformidade. O mecanismo de byte range é o mesmo; a promessa sobre o futuro é diferente.

O engine do NextPDF implementa exatamente isso: um placeholder ByteRange de largura fixa, o digest concatenado de dois segmentos e um objeto CMS destacado em um espaço Contents reservado, finalizado somente depois que o arquivo está completo.

Você raramente monta um ByteRange manualmente. O objetivo do exemplo é mostrar o formato do resultado para que você o reconheça ao inspecionar um arquivo assinado.

<?php
declare(strict_types=1);
use NextPDF\Security\Signature\ByteRangeCalculator;
// Offsets the engine knows only after the whole PDF is written:
// $contentsStart — byte just before the '<' of the hex signature
// $contentsEnd — byte just after the '>' that closes it
// $fileLength — total file size in bytes
$range = ByteRangeCalculator::calculate(
contentsStart: $contentsStart,
contentsEnd: $contentsEnd,
fileLength: $fileLength,
);
// $range === [0, $contentsStart, $contentsEnd, $fileLength - $contentsEnd]
// Segment 1: file start → just before the signature value
// Segment 2: just after the signature value → end of file
// The signature value itself is the gap. It is never hashed.
$signedMessage = ByteRangeCalculator::extractSignedData($pdfBytes, $range);
// $signedMessage is segment 1 concatenated with segment 2 — exactly the
// bytes the cryptographic digest is computed over.

A lacuna entre os dois segmentos é o valor da assinatura. Ele não pode fazer parte do próprio digest; por isso, o intervalo tem duas partes, não uma.

A armadilha é acreditar que uma assinatura válida significa que o arquivo inteiro que você está vendo foi assinado. Não significa. Significa que os bytes dentro do intervalo declarado estão intactos. Uma revisão posterior pode legitimamente anexar conteúdo — uma segunda assinatura, dados de formulário, material de validação — fora desse intervalo. A primeira assinatura permanece válida e não diz nada sobre o acréscimo. Um visualizador correto informa que uma assinatura cobre “o documento tal como ele existia no momento da assinatura”, não “todo byte na tela”. Tratar os dois como a mesma coisa é o que permite que um documento assinado ganhe conteúdo não assinado com aparência de assinado.

Esta página explica a estrutura, não a confiança. Um ByteRange corretamente formado e um objeto CMS informam que os bytes estão intactos e qual chave os assinou. Eles não informam, por si só, se essa chave pertence a quem você pensa, se o certificado dela era válido no momento da assinatura ou se ele foi posteriormente revogado. Isso é trabalho de caminho de certificação e revogação, abordado em Validar uma assinatura corretamente. Esta página também não cobre quando a assinatura ocorreu com qualquer autoridade independente. Um horário de assinatura autodeclarado não é horário confiável — consulte Carimbos de tempo e horário confiável. O NextPDF constrói a estrutura descrita aqui; os certificados, as âncoras de confiança e a autoridade de carimbo de tempo são fornecidos pela sua implantação, não pelo engine.

O que o engine entrega, por tier, é a capacidade de construir a estrutura:

PAdES signature structure (byte range, signature dictionary, detached CMS) — edition availability
Edition Availability
Core

PAdES B-B: o dicionário de assinatura, o ByteRange de largura fixa e o objeto CMS SignedData destacado descrito nesta página.

Pro

Adiciona PAdES B-T — um carimbo de tempo confiável sobre o valor da assinatura — à mesma estrutura.

Enterprise

Adiciona os perfis de longo prazo (B-LT, B-LTA): material de validação embutido e carimbos de tempo de documento sobrepostos à mesma base de byte range.

  • Dicionário de assinatura — o dicionário do PDF que identifica o handler de assinatura, o SubFilter, o ByteRange e o valor de Contents.
  • ByteRange — um array de pares de inteiros (offset, length) que declaram os bytes exatos que o digest da assinatura cobre.
  • Contents — a entrada hexadecimal que contém o valor da assinatura (para PDF 2.0, um objeto CMS SignedData destacado); ela é excluída do próprio digest.
  • CMS SignedData — estrutura Cryptographic Message Syntax (RFC 5652) que carrega o certificado do signatário e os bytes da assinatura.
  • PAdES — PDF Advanced Electronic Signatures: o perfil ETSI de assinaturas CMS para PDF, definido na série ETSI EN 319 142.
  • Assinatura de aprovação — uma assinatura com a permissão MDP 0; ela atesta o conteúdo sem restringir alterações posteriores.
  • Assinatura de certificação — uma assinatura com uma permissão DocMDP (MDP 13) que limita quais modificações posteriores mantêm o documento em conformidade.