Uma assinatura sobre um PDF não proíbe mudanças posteriores. Ela fixa um intervalo de bytes, e uma atualização incremental anexa novos bytes depois dele, então a assinatura permanece 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 declara a política: nenhuma mudança, apenas preenchimento de formulário e assinatura, ou isso mais anotações. Aplicá-la significa classificar o que de fato mudou, que é o que o AnalyzeModifications faz. Aponte-o para uma revisão anterior e leia o GetModificationLevel para o veredito geral e os accessors por finding para o nível, o número de objeto e a descrição de cada diferença
Com isso no lugar, a aplicação do DocMDP colapsa em uma comparação: o nível calculado está no nível que a política permite ou abaixo dele
Por que um PDF assinado deve mudar
Três casos legítimos, e eles cobrem a maior parte do que você verá. Um segundo signatário adiciona 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 document security store para que a assinatura permaneça verificável depois que os respondedores desaparecerem. Esse último caso não é apenas permitido, é o que um arquivo bem gerido faz com documentos assinados de propósito
Então "o arquivo cresceu depois da assinatura" não carrega informação alguma. A pergunta é sempre o que foi adicionado, e a resposta tem de vir de comparar estados do documento em vez de observar bytes. A mecânica de append em si é coberta em o artigo sobre atualização incremental
Classifique pela forma do objeto, não pelo caminho que o produziu
O classificador olha o que um objeto é depois da mudança, não qual chamada de biblioteca o criou. Isso é deliberado, porque a análise roda contra arquivos produzidos por outros softwares, onde não há caminho de chamada a inspecionar
Quatro formas são reconhecidas. Dicionários information de document security store e relacionados à validação, objetos cross-reference stream, a entrada metadata do catálogo e dicionários de assinatura que carregam um byte range são material de arquivo de longo prazo. Um objeto que carrega tanto um field type quanto um field value é preenchimento de formulário. Um objeto cujo tipo é annotation, ou cujo subtype é um dos listados na Tabela 168 da ISO 32000-2, é uma mudança de anotação. Todo o resto é unclassified
Remoções são tratadas com mais rigor do que adições. Um objeto removido só entra na whitelist quando o objeto no lado antigo era ele próprio material de arquivo, o que cobre o caso normal de um security store substituído por um mais novo. Toda outra remoção é unclassified, porque excluir conteúdo de um documento assinado não é algo que um nível de permissão autorize. Diferenças no nível do documento são ainda mais rigorosas: uma mudança na contagem de páginas vai direto a unclassified sem examinar objetos individuais, já que nenhum nível DocMDP permite adicionar ou remover páginas
A whitelist erra no sentido de recusar
Esta é a regra de design que governa toda decisão de fronteira. Uma mudança erroneamente classificada como permitida é uma assinatura que valida sobre conteúdo que o autor nunca autorizou. Uma mudança erroneamente classificada como unclassified é um documento que fica marcado e revisado por uma pessoa. Esses dois erros não são simétricos, então a whitelist permanece estreita e formas não reconhecidas caem em unclassified em vez de serem adivinhadas
Isso tem uma consequência prática que vale antecipar: arquivos de produtores incomuns às vezes reportarão mudanças unclassified que, ao inspecionar, são benignas. A resposta certa é olhar o detalhe do finding 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 controle 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 retorna seu ordinal
Level := Pdf.GetModificationLevel;
// A aplicaçã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 geral é o máximo sobre todos os findings, que é a única agregação defensável: um documento contendo noventa e nove adições de arquivo e uma mudança unclassified é uma mudança unclassified
Por baixo: fingerprints, 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 um fingerprint de 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 deve sempre produzir o mesmo fingerprint 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á venceu por outros meios, e pagar um hash criptográfico completo sobre cada objeto em um documento de um milhão de objetos é um custo real sem benefício
Duas regras de normalização importam mais que a escolha do hash. Referências indiretas se dobram em um token de placeholder em vez de serem expandidas no conteúdo referenciado: expandir copiaria o corpo de um objeto compartilhado em cada referenciador, então uma pequena edição em um font descriptor compartilhado invalidaria o fingerprint de cada objeto que o alcança, e o relatório seria ilegível. E os números de objeto em si são excluídos do fingerprint, porque uma reescrita pode renumerar objetos sem mudar nada semântico
O casamento então roda em duas passadas, alinhando primeiro por fingerprint e pareando o restante por número de objeto para identificar mudanças em vez de uma adição mais uma remoção. 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
Uma armadilha: a autocomparação não é garantida de ser idêntica
O primeiro teste natural para um motor de diff é comparar um arquivo consigo mesmo e afirmar que o resultado é idêntico. Essa afirmação não vale aqui, e a razão é instrutiva. O caminho público de carregamento e o caminho de carregamento de documento de nível mais baixo não configuram a decodificação de forma idêntica, então o mesmo arquivo carregado pelas duas rotas pode produzir fingerprints que diferem para alguns objetos. O motor não está errado; os dois carregamentos genuinamente produziram estados diferentes na memória
Em vez de forçar os dois caminhos 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 apenas quando os dois conjuntos de fingerprints coincidem exatamente. Essa é a pergunta que os usuários realmente fazem, e ela não exige que os dois loaders sejam intercambiáveis. Quando você está projetando um recurso de comparação, definir o que "o mesmo" significa é mais trabalho do que calculá-lo
Onde usar
Dois lugares. Em um relatório de validação, ao lado da verificação de assinatura, para que um revisor veja não apenas se a assinatura está criptograficamente intacta, mas o que aconteceu com o documento depois; o lado da assinatura é coberto em assinatura e validação PAdES. E em um gate de admissão, onde um documento que chega de fora é conferido com a cópia que você enviou, para que um contrato devolvido com uma anotação adicionada seja tratado de forma diferente de um com uma página editada
Uma ressalva sobre escopo. Esta análise diz o que mudou entre duas revisões da mesma linhagem de documento. Ela não diz se o conteúdo visível é enganoso, se o appearance stream de um 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 de remoção de conteúdo é coberto em o artigo sobre redaction verdadeira. Os pontos de entrada de análise e comparação estão documentados na página de produto da losLab PDF Developer Library