Artigo Técnico

Classificar o que mudou num PDF depois de assinado

Uma assinatura sobre um PDF não proíbe alterações posteriores. Fixa um intervalo de bytes, e uma atualização incremental acrescenta novos bytes depois dela, pelo que a assinatura se mantém matematicamente válida enquanto o documento adquire novo conteúdo. Se esse conteúdo é aceitável é uma questão de política, e o DocMDP é onde o autor enuncia a política: nenhuma alteração de todo, só preenchimento de formulários e assinatura, ou isso mais anotações. Impô-la significa classificar o que realmente mudou, que é o que AnalyzeModifications faz. Aponte-a para uma revisão anterior, e depois leia GetModificationLevel para o veredicto global e os acessores por descoberta para o nível, número de objeto e descrição de cada diferença

Com isso no lugar, a imposição do DocMDP colapsa numa comparação: o nível calculado está no nível que a política permite ou abaixo dele

Diagrama da escada de níveis de modificação do PDFlibPas de mlNone a mlUnclassified mostrando a imposição da política DocMDP como uma comparação em Delphi
A escada TPLModificationLevel vai de mlNone a mlUnclassified, e a imposição do DocMDP reduz-se a comparar o nível calculado com a política

Porque é que se espera que um PDF assinado mude

Três casos legítimos, e cobrem a maior parte do que vai ver. Um segundo signatário acrescenta a sua assinatura. Um destinatário preenche campos de formulário que o autor deixou abertos. E material de validação de longo prazo é anexado: respostas OCSP e CRLs escritas no documento security store para que a assinatura permaneça verificável depois de os respondedores desaparecerem. Esse último caso não é apenas permitido, é o que um arquivo bem gerido faz deliberadamente a documentos assinados

Assim, "o ficheiro cresceu depois de assinar" não transporta informação nenhuma. A questão é sempre o que foi acrescentado, e a resposta tem de vir de comparar estados do documento em vez de observar bytes. A mecânica de acréscimo em si está coberta no artigo sobre atualização incremental

Classificar pela forma do objeto, não pelo caminho que o produziu

O classificador olha para o que um objeto é depois da alteração, não para que chamada de biblioteca o criou. Isso é deliberado, porque a análise corre contra ficheiros produzidos por outro software, onde não há caminho de chamada nenhum para inspecionar

Quatro formas são reconhecidas. Dicionários de informação do documento security store e relacionados com validação, objetos de cross-reference stream, a entrada de metadados do catálogo, e dicionários de assinatura que transportam um intervalo de bytes são material de arquivo de longo prazo. Um objeto que transporta tanto um tipo de campo como um valor de campo é preenchimento de formulário. Um objeto cujo tipo é annotation, ou cujo subtipo é um dos listados na Tabela 168 da ISO 32000-2, é uma alteração de anotação. Tudo o resto não é classificado

Árvore de decisão que o PDFlibPas aplica a cada objeto PDF alterado, ordenando atualizações em níveis de arquivo, preenchimento de formulário, anotação ou não classificado
Cada objeto alterado é classificado pelo que é — security store, xref stream, campo, anotação — nunca pela chamada que o produziu

As remoções são tratadas mais estritamente do que as adições. Um objeto removido só é colocado na whitelist quando o objeto do lado antigo era ele próprio material de arquivo, o que cobre o caso normal de um security store substituído por um mais recente. Todas as outras remoções não são classificadas, porque apagar conteúdo de um documento assinado não é algo que um nível de permissão autorize. As diferenças ao nível do documento são ainda mais estritas: uma alteração na contagem de páginas vai direta a não classificada sem examinar objetos individuais, já que nenhum nível DocMDP permite acrescentar ou remover páginas

A whitelist erra no sentido de recusar

Esta é a regra de design que rege toda a decisão limítrofe. Uma alteração classificada erradamente como permitida é uma assinatura que valida sobre conteúdo que o autor nunca autorizou. Uma alteração classificada erradamente como não classificada é um documento que fica sinalizado e é revisto por uma pessoa. Esses dois erros não são simétricos, pelo que a whitelist se mantém estreita e as formas não reconhecidas caem em não classificado em vez de serem adivinhadas

Isso tem uma consequência prática que vale a pena antecipar: ficheiros de produtores invulgares por vezes reportarão alterações não classificadas que, à inspeção, são benignas. A resposta certa é olhar para o detalhe da descoberta e o número de objeto em vez de alargar a whitelist, porque uma whitelist que cresce para silenciar relatórios individuais deixa de ser um controlo de segurança

uses
  PDFlibrary, PDFlibCompare;

var
  Pdf: TPDFlib;
  I, Level: Integer;
begin
  Pdf := TPDFlib.Create(nil);
  try
    Pdf.LoadFromFile('contract-countersigned.pdf', '');
    if Pdf.AnalyzeModifications('contract-as-signed.pdf', '') < 0 then
      raise Exception.Create('the earlier revision could not be loaded');

    // TPLModificationLevel ordenado mlNone, mlLTAUpdates, mlFormFilling,
    // mlAnnotations, mlUnclassified; o getter devolve o seu ordinal
    Level := Pdf.GetModificationLevel;
    // A imposição do DocMDP é agora uma comparação contra a política
    if Level > Ord(mlFormFilling) then
      for I := 0 to Pdf.GetModificationFindingCount - 1 do
        Report.Add(Format('object %d, level %d: %s',
          [Pdf.GetModificationFindingObjNum(I),
           Pdf.GetModificationFindingLevel(I),
           Pdf.GetModificationFindingDetail(I)]));
  finally
    Pdf.Free;
  end;
end;

O nível global é o máximo sobre todas as descobertas, que é a única agregação defensável: um documento contendo noventa e nove adições de arquivo e uma alteração não classificada é uma alteração não classificada

Por baixo: impressões digitais, não hashes criptográficos

O motor de comparação que o CompareWith expõe, e sobre o qual a análise de modificações é construída, identifica objetos por uma impressão digital do seu corpo normalizado usando um hash de 64 bits não criptográfico em vez de SHA-256. Essa é uma escolha ponderada. O que a comparação estrutural precisa é de determinismo: o mesmo corpo de objeto tem de produzir sempre a mesma impressão digital dentro de uma execução. Não precisa de resistência a colisões, porque um atacante que controla ambos os lados da comparação já ganhou por outros meios, e pagar um hash criptográfico completo sobre cada objeto num documento de um milhão de objetos é um custo real sem benefício

Duas regras de normalização importam mais do que a escolha do hash. As referências indiretas colapsam num token de marcador em vez de serem expandidas para o conteúdo referenciado: expandir copiaria o corpo de um objeto partilhado para cada referenciador, pelo que uma pequena edição num descritor de fonte partilhado invalidaria a impressão digital de todos os objetos que o alcançam, e o relatório ficaria ilegível. E os números de objetos em si estão excluídos da impressão digital, porque uma reescrita pode renumerar objetos sem mudar nada semântico

O emparelhamento corre depois em duas passagens, alinhando primeiro por impressão digital e emparelhando o resto por número de objeto para identificar alterações em vez de uma adição mais uma remoção. As verificações baratas vêm primeiro ao longo de tudo: uma diferença de contagem de páginas é reportada antes de qualquer travessia de objetos começar

Diff de revisões PDF em duas passagens no PDFlibPas: verificação da contagem de páginas primeiro, impressões digitais de 64 bits, alinhamento por impressão digital e depois emparelhamento por número de objeto
O motor de comparação tira impressões digitais dos corpos de objetos normalizados, reporta primeiro diferenças de contagem de páginas, e depois emparelha por impressão digital e número de objeto

Uma armadilha: a autocomparação não é garantidamente idêntica

O primeiro teste natural para um motor de diff é comparar um ficheiro consigo próprio e afirmar que o resultado é idêntico. Essa asserção não se sustenta aqui, e a razão é instrutiva. O caminho de carregamento público e o caminho de carregamento de documento de nível mais baixo não configuram a descodificação de forma idêntica, pelo que o mesmo ficheiro carregado pelas duas rotas pode produzir impressões digitais que diferem para alguns objetos. O motor não está errado; os dois carregamentos produziram genuinamente estados diferentes em memória

Em vez de forçar os dois caminhos a juntos, a semântica de comparação é enunciada de forma estreita: a análise compara o estado atual do documento com uma revisão anterior, e reporta idêntico só quando os dois conjuntos de impressões digitais coincidem exatamente. Essa é a pergunta que os utilizadores realmente fazem, e não exige que os dois carregadores sejam intercambiáveis. Ao desenhar uma funcionalidade de comparação, definir o que "o mesmo" significa é mais do trabalho do que calculá-lo

Onde utilizar

Dois lugares. Num relatório de validação, ao lado da verificação de assinaturas, para que um revisor veja não só se a assinatura está criptograficamente intacta mas o que aconteceu ao documento depois; o lado das assinaturas está coberto em assinatura e validação PAdES. E num portão de entrada, onde um documento que chega de fora é confrontado com a cópia que enviou, para que um contrato devolvido com uma anotação acrescentada seja tratado de forma diferente de um com uma página editada

Uma ressalva sobre o âmbito. Esta análise diz-lhe o que mudou entre duas revisões da mesma linhagem de documentos. Não lhe diz se o conteúdo visível é enganador, se um appearance stream de campo de formulário corresponde ao seu valor, ou se texto escondido sob uma sobreposição ainda está presente no content stream. Isso precisa de tratamento separado, e o lado da remoção de conteúdo está coberto no artigo sobre redação verdadeira. Os pontos de entrada de análise e comparação estão documentados na página de produto da losLab PDF Developer Library