Um PDF assinado que mudou depois de assinado não está automaticamente danificado. A ISO 32000-1 permite atualizações incrementais sobre uma assinatura, e apenas algumas quebram a política que o signatário definiu. O HotPDF Component para Delphi e C++Builder responde a essa questão com AnalyzeLoadedSignatureRevisions, que classifica cada revisão pós-assinatura e a avalia contra DocMDP e FieldMDP. O cenário é familiar para quem envia software de contratos: o seu cliente assina um contrato de compra, envia-o, e recebe-o de volta com uma página de anexo acrescentada. O leitor mostra uma barra amarela a dizer 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 de trabalho normal de contra-assinatura ou alguém a editar silenciosamente um contrato assinado
O que conta como uma alteração legal após a assinatura?
Uma alteração é legal quando a 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 quaisquer alterações, 2 permite preencher formulários e assinar, 3 permite preencher formulários, assinar e anotações. O HotPDF expõe-nos como valores THPDFDocMDPPermission dmpNoChanges, dmpFormFillAndSign e dmpFormFillSignAndAnnotate, com dmpNone reservado para resultados de inspeção sem qualquer transformação DocMDP
As categorias estão ordenadas, e essa ordenação é o motor de toda a verificação. THPDFRevisionModificationLevel executa rmlNone, rmlLongTermValidation, rmlFormFillAndSign, rmlAnnotations, rmlOther, deliberadamente organizados para que um ordinal maior nunca seja menos restritivo. Um documento inteiro reduz-se ao nível máximo observado em todas as revisões após a assinatura, e a comparação DocMDP torna-se um único teste de inteiros. Uma nuance importa desde já: em dmpNoChanges, a análise ainda aceita rmlLongTermValidation. Adicionar material de validação DSS e VRI ou um selo temporal de documento a um ficheiro certificado é manutenção da assinatura, não modificação do documento, e tratá-lo como violação quebraria todos os fluxos de trabalho de arquivo de longo prazo existentes
Como reconstrói o HotPDF a cadeia de revisões?
Estruturalmente, não de forma heurística. Segundo a ISO 32000-1 §7.5.6, uma atualização incremental anexa uma nova secção de referência cruzada cujo /Prev aponta para a anterior, pelo que o HotPDF lê startxref a partir do final, analisa a secção aí, segue /Prev para trás e repete, devolvendo as secções da mais antiga para a mais recente. Dois limites de segurança estão nesse ciclo e ambos valem a pena conhecer ao fazer triagem de um ficheiro que falha: um /Prev que aponta para um deslocamento já visitado termina a passagem com um diagnóstico de ciclo explícito em vez de girar sem fim, e uma cadeia com mais de mil revisões é rejeitada por completo. Ambos surgem em Analysis.Issue com a função a devolver False, e nenhum deles deve ser ignorado, porque um /Prev cíclico é um ficheiro malformado ou hostil, não invulgar
Quatro formas históricas aparecem em documentos reais e todas as quatro são tratadas: tabelas xref tradicionais analisadas linha a linha, cross-reference streams descomprimidos e descodificados através dos seus campos /W e /Index, ficheiros de referência híbrida cujo trailer tradicional transporta uma chave /XRefStm que é analisada e fundida na mesma revisão (o caso do produtor Office, coberto em o artigo sobre cross-reference streams híbridos), e objetos que vivem dentro de um contentor ObjStm, que importam porque uma atualização moderna normalmente coloca o dicionário alterado num stream comprimido em vez de o escrever diretamente, como descrito em o artigo sobre object streams e atualizações incrementais. A assinatura ancora a divisão: /ByteRange[2] + /ByteRange[3] torna-se SignedRevisionLength, e cada secção nesse deslocamento ou além dele é pós-assinatura. Se o intervalo de bytes ainda faz hash corretamente é uma questão separada, respondida por VerifyLoadedSignature e coberta em o artigo sobre verificar assinaturas digitais de PDF
Como é classificado cada objeto alterado
A classificação corre por objeto, depois propaga-se ao longo das referências. Para cada número de objeto que uma secção pós-assinatura toca, o HotPDF lê o novo corpo e o corpo tal como estava na captura assinada; um corpo idêntico é rmlNone, porque os produtores reescrevem objetos sem os alterar. Os reconhecedores são estreitos de propósito. Um objeto /Type /DocTimeStamp, ou um cujo /SubFilter seja ETSI.RFC3161, é rmlLongTermValidation, tal como qualquer coisa alcançável a partir da árvore /DSS do catálogo; um dicionário /Type /Sig é rmlFormFillAndSign. Para contentores, o teste é sobre que chaves se moveram, não 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, o que é exatamente como a página de anexo acrescentada é apanhada: acrescentar uma página reorganiza a árvore de páginas de formas que nenhuma lista branca cobre, e nenhuma quantidade de preenchimento legítimo de formulário se assemelha a isso
Depois os níveis propagam-se, com cada contentor a herdar o nível máximo dos filhos alterados a que aponta, iterado até a atribuição estabilizar. É isto que faz os streams de aparência funcionarem. Um campo de texto preenchido reescreve /V e aponta para um novo stream /AP, e esse stream por si só é um blob anónimo de operadores de conteúdo sem qualquer tipo a reconhecer; porque o campo que o possui é rmlFormFillAndSign, o stream herda o mesmo nível em vez de cair para rmlOther. A mesma propagação transporta o contexto DSS para streams de certificado e revogação que de outro modo seriam inclassificáveis
Por que conta um objeto ilegível como uma violação?
Porque a alternativa é um validador derrotado ao escrever algo que não entende. Três situações terminam em rmlOther sem recurso no HotPDF: um objeto cujo corpo não pôde ser lido a partir da revisão, um objeto que a revisão marca como livre, e um objeto que não corresponde a nenhum dos reconhecedores acima. Cada um regista um diagnóstico específico no campo Issue da revisão, para que um operador consiga ver que número de objeto produziu o veredito
Libertar é o mais afiado dos três. Uma revisão pós-assinatura que marca um objeto previamente definido como livre apagou 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 acabam em FreedObjectNumbers e a revisão é elevada a rmlOther. Os objetos ilegíveis seguem a mesma lógica por uma razão diferente. Um validador que não consegue analisar um objeto não tem base para o considerar inofensivo, e a resposta honesta a isso não é o silêncio. Reportar uma construção invulgar mas benigna como uma violação custa uma revisão humana; o erro oposto envia um contrato assinado com uma edição não detetada dentro dele
Ler o veredito em Delphi
A chamada é curta. Carregue o documento, escolha um índice de assinatura, leia o registo; a sobrecarga sem parâmetros reabre o ficheiro a partir do qual o documento foi carregado, e a sobrecarga de TStream aceita bytes fornecidos por quem chama 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 interface em vez de os colapsar, e note que um documento sem transformação DocMDP deixa DocMDPCompliant em True, uma vez 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, normalmente quer a discriminação por revisão em vez do resumo, porque diz quando na história do documento as coisas correram mal. Cada entrada em Analysis.Revisions transporta o seu índice na cadeia, o deslocamento de referência cruzada onde foi escrita, o 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;
O FieldMDP é julgado separadamente, e isso é deliberado
Um documento pode satisfazer o DocMDP e continuar a ser ilegítimo, razão pela qual FieldMDPCompliant é um booleano distinto em vez de estar 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 /SigFieldLock relacionada, para congelar campos de formulário nomeados no momento da assinatura mesmo onde o documento como um todo ainda permite preencher formulários. Preencher um campo é uma ação de nível 2; preencher um campo que o signatário bloqueou é uma violação independentemente do nível. O HotPDF lê o âmbito para THPDFFieldLockAction como flaAll, flaInclude ou flaExclude, com flaNone para resultados sem qualquer política de bloqueio, e os nomes para Permissions.FieldNames: flaAll bloqueia tudo, flaInclude bloqueia os nomes listados, flaExclude bloqueia tudo exceto eles. Um detalhe importa ao ler os resultados, na medida em que apenas os campos já presentes na captura assinada são reportados em ChangedFieldNames, porque um campo criado inteiramente após a assinatura não tem estado assinado a contradizer e é apanhado pelo percurso 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 esta análise não lhe vai dizer
Não verifica uma assinatura. AnalyzeLoadedSignatureRevisions raciocina sobre estrutura e permissões; se o intervalo de bytes assinado ainda faz hash para o valor no blob CMS, e se o certificado do signatário encadeia até algo em que confia, são respondidas por VerifyLoadedSignature e VerifyLoadedSignatureWithTrust. Um ficheiro pode ser perfeitamente conforme com a política e criptograficamente sem valor, pelo que as duas verificações pertencem lado a lado em qualquer porta de aceitação real. Também não lê intenção dentro de streams de conteúdo: uma página cujo stream de conteúdo foi substituído por completo é apanhada como uma alteração fora da lista branca, mas a análise não lhe vai dizer que a substituição trocou um valor de pagamento. Um veredito rmlOther significa que um humano deve olhar, não que houve fraude, e um veredito conforme significa que a alteração se encaixa numa categoria permitida, não que a alteração era desejada. Quando tudo o que precisa é do que o signatário declarou, sem a passagem de revisões, GetLoadedSignaturePermissions devolve os dicionários de política por si só
Tudo o que aqui se descreve corre nativamente em Delphi e C++Builder sem qualquer serviço de assinatura externo no circuito, o que torna prático executá-lo em cada documento recebido em vez de apenas naqueles que alguém já suspeitava. A API completa de assinatura e revisão, incluindo os métodos de permissão e verificação com que se emparelha, faz parte do HotPDF Component para Delphi e C++Builder