Um PDF assinado que mudou depois da assinatura não está automaticamente quebrado. A ISO 32000-1 permite atualizações incrementais sobre uma assinatura, e apenas algumas delas violam a política que o signatário definiu. O HotPDF Component para Delphi e C++Builder responde a essa pergunta com AnalyzeLoadedSignatureRevisions, que classifica toda revisão pós-assinatura e a avalia contra DocMDP e FieldMDP. O cenário é familiar para quem entrega software de contratos: seu cliente assina um contrato de compra, o envia, e o recebe de volta com uma página de anexo colada. O leitor mostra uma barra amarela dizendo que a assinatura está intacta mas o documento foi alterado desde que foi assinado, e ninguém na sala consegue dizer se isso é um fluxo normal de contra-assinatura ou alguém silenciosamente editando um contrato assinado
O que conta como uma alteração legal após a assinatura?
Uma alteração é legal quando sua categoria semântica cai dentro da permissão que a assinatura de certificação declarou. A ISO 32000-1 §12.8.2.2 define a transformação DocMDP com um valor /P de 1, 2 ou 3: 1 não permite nenhuma alteração, 2 permite preenchimento de formulário e assinatura, 3 permite preenchimento de formulário, assinatura e anotações. O HotPDF expõe isso como valores THPDFDocMDPPermission dmpNoChanges, dmpFormFillAndSign e dmpFormFillSignAndAnnotate, com dmpNone reservado para resultados de inspeção que não carregam nenhuma transformação DocMDP
As categorias são ordenadas, e essa ordenação é o motor de toda a verificação. THPDFRevisionModificationLevel percorre rmlNone, rmlLongTermValidation, rmlFormFillAndSign, rmlAnnotations, rmlOther, deliberadamente organizados de modo que um ordinal maior nunca é menos restritivo. Um documento inteiro se reduz ao nível máximo observado em todas as revisões após a assinatura, e a comparação com DocMDP se torna um único teste de inteiro. Uma nuance importa desde cedo: em dmpNoChanges a análise ainda aceita rmlLongTermValidation. Adicionar material de validação DSS e VRI ou um carimbo de tempo de documento a um arquivo certificado é manutenção da assinatura, não modificação do documento, e tratar isso como violação quebraria todo fluxo de arquivamento de longo prazo existente
Como o HotPDF reconstrói a cadeia de revisões?
Estruturalmente, não heuristicamente. Conforme a ISO 32000-1 §7.5.6, uma atualização incremental anexa uma nova seção de referência cruzada cujo /Prev aponta para a anterior, então o HotPDF lê startxref a partir do final, analisa a seção ali, segue /Prev para trás e repete, retornando as seções da mais antiga para a mais recente. Dois limites de segurança ficam nesse laço e ambos valem a pena conhecer ao triar um arquivo que falha: um /Prev apontando para um offset já visitado termina a caminhada com um diagnóstico explícito de ciclo em vez de girar indefinidamente, e uma cadeia mais longa que mil revisões é rejeitada de imediato. Ambos aparecem em Analysis.Issue com a função retornando False, e nenhum dos dois deveria ser ignorado, porque um /Prev cíclico é um arquivo malformado ou hostil, não um caso apenas incomum
Quatro formatos históricos aparecem em documentos reais e todos os quatro são tratados: tabelas xref tradicionais analisadas linha por linha, cross-reference streams descomprimidos e decodificados através de seus campos /W e /Index, arquivos de referência híbrida cujo trailer tradicional carrega uma chave /XRefStm que é analisada e mesclada na mesma revisão (o caso de produtores Office, coberto em o artigo sobre cross-reference streams híbridos), e objetos vivendo dentro de um contêiner ObjStm, que importam porque uma atualização moderna geralmente coloca o dicionário alterado em um stream comprimido em vez de escrevê-lo diretamente, como descrito em o texto sobre object streams e atualizações incrementais. A assinatura ancora a divisão: /ByteRange[2] + /ByteRange[3] se torna SignedRevisionLength, e toda seção nesse offset ou além é pós-assinatura. Se o intervalo de bytes ainda hasheia corretamente é uma questão separada, respondida por VerifyLoadedSignature e coberta em o artigo sobre verificação de assinaturas digitais de PDF
Como cada objeto alterado é classificado
A classificação roda por objeto, depois se propaga ao longo das referências. Para cada número de objeto que uma seção pós-assinatura toca, o HotPDF lê o novo corpo e o corpo como ele estava no instantâneo assinado; um corpo idêntico é rmlNone, porque produtores de fato reescrevem objetos sem alterá-los. Os reconhecedores são estreitos de propósito. Um objeto /Type /DocTimeStamp, ou um cujo /SubFilter é ETSI.RFC3161, é rmlLongTermValidation, assim como qualquer coisa alcançável a partir da árvore /DSS do catálogo; um dicionário /Type /Sig é rmlFormFillAndSign. Para contêineres o teste é sobre quais chaves se moveram, não sobre o que o objeto é: o catálogo só pode ganhar ou alterar /DSS, /Extensions ou /AcroForm; o dicionário AcroForm apenas /Fields, /SigFlags, /NeedAppearances, /DR, /DA ou /Q; uma página apenas /Annots; um campo ou widget apenas /V, /AP, /AS ou /M. Qualquer coisa fora desses conjuntos cai para rmlOther, que é exatamente como a página de anexo adicionada é capturada: adicionar uma página rearranja a árvore de páginas de formas que nenhuma lista de permissões cobre, e nenhum preenchimento legítimo de formulário se parece com isso
Depois os níveis se propagam, com cada contêiner herdando o nível máximo dos filhos alterados que ele aponta, iterado até que a atribuição se estabilize. Isso é o que faz os appearance streams funcionarem. Um campo de texto preenchido reescreve /V e aponta para um /AP novo, e esse stream sozinho é um blob anônimo de operadores de conteúdo sem nenhum tipo a reconhecer; como o campo que o possui é rmlFormFillAndSign, o stream herda o mesmo nível em vez de cair para rmlOther. A mesma propagação leva o contexto DSS para streams de certificado e revogação que de outra forma seriam inclassificáveis
Por que um objeto ilegível conta como violação?
Porque a alternativa é um validador derrotado ao escrever algo que ele não entende. Três situações terminam em rmlOther sem apelação no HotPDF: um objeto cujo corpo não pôde ser lido da revisão, um objeto que a revisão marca como liberado, e um objeto que não corresponde a nenhum dos reconhecedores acima. Cada um registra um diagnóstico específico no campo Issue da revisão, para que um operador possa ver qual número de objeto produziu o veredito
Liberação é a mais nítida das três. Uma revisão pós-assinatura que marca um objeto previamente definido como livre excluiu conteúdo de um documento assinado, e nenhum nível de permissão sob a §12.8.2.2 permite isso; os números de objeto vão para FreedObjectNumbers e a revisão é elevada para rmlOther. Objetos ilegíveis seguem a mesma lógica por um motivo diferente. Um validador que não consegue analisar um objeto não tem base para chamá-lo de inofensivo, e a resposta honesta para isso não é o silêncio. Reportar uma construção incomum mas benigna como violação custa uma revisão humana; o erro oposto entrega um contrato assinado com uma edição não notada dentro dele
Lendo o veredito em Delphi
A chamada é curta. Carregue o documento, escolha um índice de assinatura, leia o registro; a sobrecarga sem parâmetros reabre o arquivo do qual o documento foi carregado, e a sobrecarga TStream recebe bytes fornecidos pelo chamador e restaura a posição do stream antes de retornar. PolicyCompliant é o único booleano que a maioria dos chamadores quer, combinando três decisões independentes: a validade estrutural dos dicionários de permissão, DocMDPCompliant, e FieldMDPCompliant. Mantenha os componentes visíveis na sua UI em vez de colapsá-los, e observe que um documento sem transformação DocMDP deixa DocMDPCompliant como True, já que uma assinatura de aprovação comum não declara nenhuma política a violar e o ModificationLevel agregado é então descritivo em vez de um veredito
var
Pdf: THotPDF;
Analysis: THPDFSignatureRevisionAnalysis;
begin
Pdf := THotPDF.Create(nil);
try
if Pdf.LoadFromFile('contract-countersigned.pdf') > 0 then
begin
if Pdf.AnalyzeLoadedSignatureRevisions(0, Analysis) then
begin
if Analysis.PolicyCompliant then
Writeln('Post-signature changes stay inside the signing policy')
else
Writeln('Policy violation: ', string(Analysis.Issue));
end
else
Writeln('Analysis could not run: ', string(Analysis.Issue));
end;
finally
Pdf.Free;
end;
end;
Para triagem você geralmente quer o detalhamento por revisão em vez do resumo, porque ele diz em que momento da história do documento algo deu errado. Cada entrada em Analysis.Revisions carrega seu índice na cadeia, o offset de referência cruzada em que foi escrita, seu próprio nível de modificação, e os números de objeto envolvidos
const
LevelNames: array[THPDFRevisionModificationLevel] of string =
('none', 'long-term validation', 'form fill and sign',
'annotations', 'other');
var
I: Integer;
begin
Writeln(Format('%d revisions in chain, signature sits at index %d',
[Analysis.TotalRevisionCount, Analysis.SignedRevisionIndex]));
for I := 0 to High(Analysis.Revisions) do
Writeln(Format(' rev %d at offset %d: %s (%d changed, %d freed) %s',
[Analysis.Revisions[I].RevisionIndex,
Analysis.Revisions[I].XRefOffset,
LevelNames[Analysis.Revisions[I].ModificationLevel],
Length(Analysis.Revisions[I].ChangedObjectNumbers),
Length(Analysis.Revisions[I].FreedObjectNumbers),
string(Analysis.Revisions[I].Issue)]));
end;
FieldMDP é julgado separadamente, e isso é deliberado
Um documento pode satisfazer DocMDP e ainda assim ser ilegítimo, motivo pelo qual FieldMDPCompliant é um booleano distinto em vez de ser dobrado na comparação de nível. A ISO 32000-1 §12.8.2.4 define a transformação FieldMDP, e a §12.7.5.5 a entrada relacionada /SigFieldLock, para congelar campos de formulário nomeados no momento da assinatura mesmo onde o documento como um todo ainda permite preenchimento de formulário. Preencher um campo é uma ação de nível 2; preencher um campo que o signatário trancou é uma violação independentemente do nível. O HotPDF lê o escopo em THPDFFieldLockAction como flaAll, flaInclude ou flaExclude, com flaNone para resultados que não carregam nenhuma política de trava, e os nomes em Permissions.FieldNames: flaAll tranca tudo, flaInclude tranca os nomes listados, flaExclude tranca tudo exceto eles. Um detalhe importa ao ler os resultados: apenas campos já presentes no instantâneo assinado são reportados em ChangedFieldNames, porque um campo criado inteiramente após a assinatura não tem nenhum estado assinado para contradizer e é capturado pelo caminho DocMDP em vez disso
var
Source: TFileStream;
Analysis: THPDFSignatureRevisionAnalysis;
I: Integer;
begin
Source := TFileStream.Create('contract.pdf', fmOpenRead or fmShareDenyWrite);
try
if Pdf.AnalyzeLoadedSignatureRevisions(0, Source, Analysis) then
if Analysis.Permissions.HasFieldMDP and (not Analysis.FieldMDPCompliant) then
for I := 0 to High(Analysis.ChangedFieldNames) do
Writeln('modified after locking: ',
string(Analysis.ChangedFieldNames[I]));
finally
Source.Free; // stream position was restored before the call returned
end;
end;
O que essa análise não vai dizer a você
Ela não verifica uma assinatura. AnalyzeLoadedSignatureRevisions raciocina sobre estrutura e permissões; se o intervalo de bytes assinado ainda hasheia para o valor no blob CMS, e se o certificado do signatário encadeia para algo em que você confia, são respondidas por VerifyLoadedSignature e VerifyLoadedSignatureWithTrust. Um arquivo pode ser perfeitamente compatível com a política e criptograficamente inútil, então as duas verificações pertencem lado a lado em qualquer portão real de aceitação. Ela também não lê intenção dentro de content streams: uma página cujo content stream foi substituído por completo é capturada como uma alteração fora da lista de permissões, mas a análise não vai dizer a você que a substituição trocou um valor de pagamento. Um veredito rmlOther significa que um humano deveria olhar, não que houve fraude, e um veredito compatível significa que a alteração se encaixa em uma categoria permitida, não que a alteração era desejada. Quando tudo que você precisa é o que o signatário declarou, sem a caminhada de revisões, GetLoadedSignaturePermissions retorna os dicionários de política por conta própria
Tudo o que foi descrito aqui roda nativamente em Delphi e C++Builder sem nenhum serviço externo de assinatura no laço, o que é o que torna prático rodar isso em todo documento de entrada em vez de apenas nos que alguém já suspeitava. A API completa de assinatura e revisão, incluindo os métodos de permissão e verificação com os quais ela se combina, faz parte do HotPDF Component para Delphi e C++Builder