Pular para o conteúdo

Atualizações incrementais e por que elas são importantes

Evidence: Standard-backed

Quando um PDF muda depois de gravado, a maneira segura de salvá-lo não é reescrever o arquivo. Em vez disso, você anexa os objetos alterados e uma nova seção de referência cruzada ao final, deixando cada byte original exatamente onde estava. Esta página explica como isso funciona e por que esse é o motivo pelo qual uma assinatura digital pode sobreviver a uma edição posterior.

Uma assinatura protege um intervalo de bytes. Se salvar uma alteração de uma única palavra reescrevesse o arquivo, cada deslocamento de byte mudaria. O intervalo assinado deixaria de descrever o mesmo conteúdo. A assinatura seria invalidada, mesmo que o conteúdo assinado em si não tivesse sido tocado.

As atualizações incrementais existem para impedir que isso aconteça. Os bytes originais, incluindo os bytes cobertos por uma assinatura, permanecem no lugar. Um revisor pode pegar um documento que foi assinado e depois editado e verificar a primeira assinatura em relação à revisão original. O revisor vê exatamente o que foi assinado e, separadamente, o que mudou depois. Se você errar nesse ponto, vai invalidar assinaturas boas ou, pior, perder a capacidade de provar o que uma assinatura de fato atestou.

  • Uma atualização incremental anexa: objetos novos e alterados, depois uma nova seção de referência cruzada e, em seguida, um novo trailer, tudo ao final do arquivo.
  • O conteúdo original do arquivo é mantido intacto — não editado no local.
  • O novo trailer carrega uma entrada /Prev: o deslocamento de byte da seção de referência cruzada anterior. As seções formam uma cadeia para trás.
  • Um leitor constrói o índice percorrendo essa cadeia do mais recente para o mais antigo. Para qualquer número de objeto, prevalece a entrada mais recente.
  • Como nada foi sobrescrito, o intervalo de bytes que uma assinatura anterior cobria continua exatamente igual, byte a byte — então a assinatura ainda é verificável, e você pode recuperar o documento exatamente como foi assinado.

O NextPDF grava o documento base como descrito na página anterior e, em seguida, expõe as três informações de que uma atualização incremental precisa.

Após build(), o writer (src/Writer/PdfWriter.php) mantém:

  • o buffer de saída, recuperável via getBuffer(), para que uma atualização possa ser anexada exatamente ao fim dos bytes existentes;
  • o deslocamento de byte da última seção de referência cruzada, via getLastXrefOffset(), que se torna o valor /Prev da nova seção;
  • as entradas do dicionário do catálogo, via getCatalogEntries(), para que uma atualização que precise reemitir o catálogo (por exemplo, para anexar uma referência de assinatura) não perca nenhuma chave anterior.

Uma revisão anexada aloca novos números de objeto (ou reutiliza os existentes para os objetos que substitui) contra o mesmo ObjectRegistry, de modo que a numeração de objetos permaneça consistente entre as revisões. A nova seção de referência cruzada lista apenas os objetos que esta revisão tocou. O novo trailer repete as entradas do trailer anterior e adiciona /Prev, apontando de volta para a seção anterior. É essa cadeia que o leitor segue.

O lugar em que isso fica mais evidente é a assinatura. O NextPDF, com seu ByteRangeCalculator (src/Security/Signature/ByteRangeCalculator.php), calcula o array /ByteRange como dois segmentos: tudo antes do valor da assinatura e tudo depois dele — de modo que a assinatura cobre toda a revisão exceto os próprios bytes. Como uma edição posterior é anexada em vez de gravada por cima desses bytes, esse intervalo nunca muda de posição.

  1. Write base revision Header, body, xref section, trailer — the original bytes.
  2. Sign A /ByteRange digest covers the whole revision except the signature value itself.
  3. Edit and save Changed objects + a new xref section are appended; originals are untouched.
  4. New trailer chains back The appended trailer carries /Prev = offset of the previous xref section.
  5. Verify The first signature still covers the same unchanged bytes; the chain shows what came after.
Como um PDF assinado que é editado posteriormente permanece verificável: cada gravação anexa, então os bytes originalmente assinados nunca são sobrescritos e a cadeia de trailers registra o histórico.

A regra de anexação exclusiva é normativa. Spec: ISO 32000-2, §7.5.6 estabelece que o conteúdo de um PDF pode ser atualizado de forma incremental sem reescrever o arquivo inteiro e que, ao fazê-lo, as alterações devem (shall) ser anexadas ao final do arquivo, deixando o conteúdo original intacto. Evidence: Standard-backed

A mesma cláusula define a mecânica. Uma seção de referência cruzada para uma atualização incremental contém entradas apenas para os objetos que foram alterados, substituídos ou excluídos. Os objetos excluídos permanecem no arquivo, mas são marcados como excluídos por meio de suas entradas de referência cruzada. O trailer adicionado deve (shall) conter uma entrada /Prev que informe a localização da seção de referência cruzada anterior. A entrada da atualização para um objeto alterado carrega o deslocamento de byte da cópia nova, substituindo o deslocamento antigo. Um leitor constrói suas informações de referência cruzada de modo que a cópia mais recente de cada objeto seja a acessada.

A consequência para a assinatura é declarada diretamente em Spec: ISO 32000-2, §12.8.1 : um digest de intervalo de bytes é calculado sobre um intervalo do arquivo — normalmente o arquivo inteiro, excluindo o valor da assinatura (a entrada /Contents). A norma então observa que se um documento assinado for modificado e salvo por atualização incremental, os dados correspondentes ao intervalo de bytes da assinatura original são preservados. Assim, se a assinatura for válida, o estado do documento no momento da assinatura pode ser recriado. A anexação exclusiva não é um luxo; é a propriedade da qual o modelo de assinatura depende.

Um PDF assinado e depois editado, visto pela estrutura. A revisão original termina em seu próprio %%EOF. A segunda revisão é anexada abaixo dela.

%PDF-2.0
... original objects, including the signature dictionary ...
xref
0 8
... entries for the original revision ...
trailer
<< /Size 8 /Root 1 0 R >>
startxref
920
%%EOF
<-- end of revision 1: the signed bytes stop here
9 0 obj <-- revision 2, appended
<< /Type /Annot /Subtype /Text /Contents (added after signing) >>
endobj
xref
0 1
9 0 obj-entry...
8 9
0000001740 00000 n
trailer
<< /Size 10 /Root 1 0 R /Prev 920 >>
startxref
1980
%%EOF

Um validador lê o último trailer, vê /Prev 920 e passa a ter toda a cadeia. Ele pode verificar a assinatura em relação aos bytes até o primeiro %%EOF, que permanecem inalterados. Ele pode, então, relatar separadamente que a revisão 2 adicionou uma anotação. O histórico está no arquivo. Nada foi ocultado por sobrescrita.

A armadilha é pensar que “atualização incremental significa que a alteração é pequena, portanto é inofensiva.” A anexação tem a ver com preservação de bytes, não com tamanho. Uma atualização incremental pode adicionar uma grande quantidade de conteúdo. O que a torna uma atualização incremental é que ela não toca nos bytes que já estavam lá. O corolário também surpreende muita gente: uma ferramenta que “otimiza” ou “lineariza” um PDF assinado reescrevendo-o do zero produzirá um arquivo menor e mais limpo e uma assinatura quebrada, porque o intervalo de bytes assinado deixa de existir. Salvar um PDF assinado e salvá-lo novamente não são a mesma operação.

A anexação exclusiva protege os bytes. Ela não diz, por si só, se as alterações anexadas foram autorizadas. Uma segunda revisão pode legitimamente adicionar uma segunda assinatura, ou pode adicionar conteúdo que o primeiro signatário nunca pretendeu. Decidir qual é o caso é tarefa da validação de assinatura e da política de detecção de modificações (DocMDP). A anexação é a base que torna essa análise possível, não a análise em si.

Esta página também não cobre como os dois intervalos de bytes de uma assinatura são calculados e unidos, nem o que uma validação completa verifica. Esses são tópicos separados. E a garantia aqui vale para arquivos gravados e atualizados por um writer em conformidade: um arquivo cujas revisões anteriores já estavam malformadas não se torna bem formado por ser anexado.

Como sei quantas revisões um PDF tem? Conte os marcadores %%EOF e siga a cadeia /Prev a partir do último trailer. Cada seção de referência cruzada encontrada é uma revisão salva.

Excluir um objeto o remove do arquivo? Não. Uma atualização incremental marca o objeto como excluído em sua entrada de referência cruzada, mas os bytes do objeto permanecem nas revisões anteriores. “Excluído” significa “não referenciado pela revisão atual”, não “apagado.”

Uma atualização incremental pode mudar a versão do PDF? Sim, definindo a entrada /Version no catálogo na revisão anexada. O cabeçalho permanece como foi gravado. O /Version do catálogo tem precedência quando nomeia uma versão posterior.

  • Atualização incremental — salvar uma alteração anexando os objetos alterados, uma nova seção de referência cruzada e um novo trailer ao fim do arquivo, sem alterar os bytes existentes.
  • /Prev — a entrada do trailer (ou do stream de referência cruzada) que contém o deslocamento de byte da seção de referência cruzada anterior. Ela liga as revisões em uma cadeia para trás.
  • Revisão — o estado do arquivo capturado por uma seção de referência cruzada e seu trailer. Um arquivo com N seções de referência cruzada tem N revisões.
  • /ByteRange — o array em um dicionário de assinatura que informa os dois segmentos de bytes que o digest da assinatura cobre (tudo, exceto o próprio valor da assinatura).
  • Intervalo de bytes assinado — os bytes exatos sobre os quais o digest de uma assinatura foi calculado. As atualizações incrementais existem para que esses bytes nunca sejam movidos ou sobrescritos.