Para descobrir o que mudou num PDF depois de assinado, o PDFium Component para Delphi e Lazarus fornece o TPdf.AnalyzeSignatureRevisions, um analisador de mudanças de revisões pós-assinatura que reconstrói cada revisão incremental a partir dos bytes originais do ficheiro, avalia cada mudança de objeto posterior contra as regras DocMDP e FieldMDP dessa assinatura, e reporta definições de objetos shadow como um risco separado. A situação a que se destina é familiar a quem trata de contratos: um formulário certificado sai, volta com mais duas gravações incrementais, e todas as assinaturas continuam a verificar. Isso é o esperado, porque uma assinatura só cobre os bytes da sua própria revisão. A pergunta real é se aquelas gravações posteriores eram permitidas, e um visto verde na assinatura não responde a isso
Porque é que a API de assinaturas do PDFium não mostra o que mudou após a assinatura?
A API de assinaturas do PDFium não consegue mostrar mudanças pós-assinatura porque só lê o dicionário da assinatura: /Contents, /ByteRange, /SubFilter e o valor de permissão DocMDP. O PDFium não tem grafo de revisões incrementais, não analisa os parâmetros de transform FieldMDP, e não oferece diff nenhum ao nível de objetos entre revisões, pelo que o analisador em FPdfPades.pas trabalha diretamente sobre os bytes brutos. Isso tem uma consequência prática para a qual deve desenhar. O TPdf.AnalyzeSignatureRevisions lê os bytes retidos quando o documento foi carregado, nunca uma cópia produzida pelo SaveAs, porque um ficheiro reescrito perdeu precisamente a estrutura de revisões que está a ser analisada. Se o documento veio de uma fonte progressiva que ainda não terminou de descarregar, o relatório devolve SourceStatus = pvssIncomplete e Status = prasIndeterminate em vez de analisar um ficheiro truncado
Reconstruir fronteiras de revisão a partir de startxref, streams xref e /Prev
O analisador reconstrói as fronteiras de revisão seguindo cada startxref para trás por tabelas xref clássicas, streams de referências cruzadas, entradas /XRefStm de referência híbrida e a cadeia /Prev, conforme definido para atualizações incrementais na ISO 32000-1 §7.5.6 e §7.5.8. O comprimento coberto por cada assinatura é o fim do seu segundo troço ByteRange, e o analisador mapeia esse comprimento na revisão cuja secção xref o contém. Quando nenhuma revisão coincide, a assinatura recebe prrCoveredRevisionNotFound e um estado Indeterminate. O estado de cada objeto é depois reprimido até à revisão coberta, e cada entrada xref posterior é comparada com esse estado. Isto importa mais do que parece: alguns escritores reenunciam a tabela xref completa em cada gravação incremental, e uma entrada que continue a apontar para o mesmo objeto inalterado é saltada em vez de ser reportada como modificação. Sem essa comparação, um preenchimento de formulário perfeitamente legal afogava-se em centenas de mudanças falsas
As definições shadow são o caso que merece mais atenção. Um corpo de objeto que aparece dentro do intervalo de bytes de uma revisão posterior mas não é referenciado pelo xref dessa revisão é invisível para um visualizador normal, e é precisamente o tipo de encenação de que os shadow attacks dependem: conteúdo escondido é plantado antes ou depois da assinatura e ativado mais tarde virando uma referência. O AnalyzePadesSignatureRevisionsBytes regista tal objeto como uma mudança não autoritativa com IsAuthoritative = False, dá-lhe prdSuspicious independentemente do nível de permissão, e acrescenta prrUnreferencedObjectDefinition ao conjunto de riscos. Dois riscos relacionados cobrem outros truques estruturais: o prrDuplicateObjectDefinition dispara quando uma secção xref lista o mesmo objeto mais do que uma vez, e o prrSignatureObjectRedefined dispara quando uma revisão posterior redefine um objeto de assinatura existente
uses
SysUtils, TypInfo, PDFium, FPdfPades;
const
ShadowTag: array[Boolean] of string = ('', ' (shadow)');
var
Pdf: TPdf;
Report: TPadesRevisionAnalysisReport;
i, j: Integer;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := 'contract-returned.pdf';
Pdf.Active := True;
Report := Pdf.AnalyzeSignatureRevisions;
Writeln('Revisions: ', Report.RevisionCount,
' Signatures: ', Report.SignatureCount,
' Overall: ', GetEnumName(TypeInfo(TPadesRevisionAnalysisStatus),
Ord(Report.Status)));
for i := 0 to High(Report.Signatures) do
with Report.Signatures[i] do
begin
Writeln(Format('Signature %d covers revision %d, %d later, P=%d, FieldMDP=%s',
[SignatureIndex, CoveredRevisionIndex, LaterRevisionCount,
DocMdpPermission,
GetEnumName(TypeInfo(TPadesFieldMdpAction), Ord(FieldMdpAction))]));
for j := 0 to High(Changes) do
Writeln(Format(' rev %d obj %d %s -> %s%s',
[Changes[j].RevisionIndex, Changes[j].ObjectNumber,
GetEnumName(TypeInfo(TPadesRevisionChangeKind), Ord(Changes[j].Kind)),
GetEnumName(TypeInfo(TPadesRevisionDecision), Ord(Changes[j].Decision)),
ShadowTag[not Changes[j].IsAuthoritative]]));
end;
finally
Pdf.Free;
end;
end;
Como são DocMDP e FieldMDP impostos para cada assinatura?
DocMDP e FieldMDP são impostos separadamente para cada assinatura, na revisão coberta dessa própria assinatura, pelo que uma assinatura de certificação e uma assinatura de aprovação posterior no mesmo ficheiro podem chegar a veredictos diferentes sobre a mesma edição. Cada objeto posterior é primeiro classificado num TPadesRevisionChangeKind a partir das suas entradas /Type, /Subtype e /FT e do papel que desempenha nos grafos de página, formulário, anotação e DSS. Tudo o que transporte /JavaScript, /JS, /Launch, /OpenAction, /AA, /RichMedia ou /EmbeddedFile torna-se prckActiveContent. A decisão segue depois a ISO 32000-1 §12.8.2.2: com P=1 tudo, exceto dados de referências cruzadas e material de validação, é proibido; o P=2 permite preencher formulários e mais assinaturas mas recusa mudanças de anotações; o P=3 também permite anotações. Conteúdo de página, estrutura do documento, metadados, conteúdo ativo e objetos apagados são proibidos sob qualquer nível DocMDP, e avaliados prdSuspicious quando a assinatura não transporta DocMDP nenhum, já que uma assinatura de aprovação nada proíbe formalmente mas o leitor já não vê o que foi assinado
O FieldMDP, da ISO 32000-1 §12.8.2.4, estreita ainda mais a decisão sobre campos de formulário. O pfmaAll bloqueia todos os campos, o pfmaInclude bloqueia apenas os campos listados, e o pfmaExclude bloqueia tudo exceto os campos listados. Para aplicar Include ou Exclude, o analisador resolve cada campo alterado até ao seu nome completamente qualificado através da cadeia /Parent e compara-o com a lista de bloqueio por correspondência exata, por isso liste nomes de campos terminais em vez de esperar que um nome de progenitor cubra os filhos. Quando um nome não pode ser resolvido ou a transform usa uma ação que o parser não reconhece, a mudança torna-se prdIndeterminate e prrFieldMdpUnresolved é levantado. As decisões por mudança agregam-se depois pior-primeiro, com Suspicious acima de Disallowed, Disallowed acima de Indeterminate, e Indeterminate acima de Allowed, pelo que um objeto shadow pesa mais do que qualquer número de atualizações legítimas de campos
Porque é que algumas mudanças voltam Indeterminate em vez de seguras?
As mudanças voltam Indeterminate sempre que o analisador não consegue provar que uma mudança é permitida, porque numa verificação de assinatura um desconhecido nunca deve ser reportado como permitido. Um caso comum é tratado com precisão: a validação de longo prazo acrescenta um /DSS e reescreve o catálogo, o que de outra forma contaria como mudança estrutural sob P=1. O analisador tira o /DSS e o /Extensions dos dicionários de catálogo antigo e novo e compara o resto; quando mais nada difere, a reescrita é tratada como uma atualização de material de validação e permitida, pelo que o aumento B-LT e B-LTA não parte uma assinatura de certificação. Outras lacunas ficam abertas de propósito. Entradas Type-2 num stream de referências cruzadas apontam para dentro de object streams comprimidos, e o analisador não expande object streams dentro desta fronteira de segurança, pelo que essas mudanças aparecem como prckCompressedObject com prrCompressedObjectUnresolved, proibidas sob P=1 e Indeterminate caso contrário. Orçamentos rígidos de 1024 revisões, 1 000 000 de números de objeto e 2 000 000 de mudanças reportadas produzem prrResourceLimitExceeded, e uma cadeia xref partida produz prrMalformedRevisionChain; ambos acabam como Indeterminate, nunca como aprovação
const
StructuralRisks: TPadesRevisionRisks = [prrMalformedRevisionChain,
prrDuplicateObjectDefinition, prrUnreferencedObjectDefinition,
prrSignatureObjectRedefined, prrResourceLimitExceeded];
function RevisionVerdict(const R: TPadesRevisionAnalysisReport): string;
begin
// Alguns riscos são registados sem alterar o Status, por isso teste-os primeiro
if R.Risks * StructuralRisks <> [] then
Exit('review: structural risk in the revision chain');
case R.Status of
prasNoLaterChanges: Result := 'accept: nothing was added after signing';
prasAllowed: Result := 'accept: every later change is permitted';
prasDisallowed: Result := 'reject: a change violates DocMDP or FieldMDP';
prasSuspicious: Result := 'reject: shadow or unconstrained content change';
prasIndeterminate: Result := 'review: the analyzer could not decide';
else
Result := 'not checked: no signatures or no original bytes';
end;
end;
A ordenação nesse portão é deliberada. O prrDuplicateObjectDefinition é acrescentado ao conjunto de riscos sem despromover por si o Status, e uma transform FieldMDP que não se consegue analisar só afeta o estado quando um campo de formulário realmente muda, pelo que um portão que olhe só para o Status pode perder evidência que o relatório já contém. Tenha também presente o que o relatório não afirma. O TPadesRevisionAnalysisReport nada diz sobre a assinatura CMS ser criptograficamente válida ou sobre o certificado do assinante encadear numa raiz em que confia. A análise de revisões responde à pergunta do que aconteceu depois da assinatura, e fica ao lado da validação estrutural e de confiança, não no lugar delas
Escrever seed values e locks MDP no momento da assinatura
As mesmas regras podem ser escritas no momento da assinatura através do TPadesSignatureFieldOptions, que é o membro FieldOptions tanto do TPadesSignOptions como do TPadesRemoteSignOptions. O PDFium consegue criar um widget mas não consegue escrever /SV, /Lock, uma transform FieldMDP ou DocMDP, nem o dicionário /Perms do catálogo, pelo que o escritor PAdES incremental do próprio componente produz estes objetos dentro da mesma atualização xref que a assinatura. O FieldName fixa o nome do campo raiz, o RequiredSeedValues torna-se os bits /Ff do dicionário de seed values descrito na ISO 32000-1 §12.7.4.5, Reasons, LegalAttestations e AcceptableCertificates restringem o que um assinante posterior pode escolher, o LockAction com LockFields escreve um /SigFieldLock indireto, e o CertificationPermission de 1 a 3 transforma a assinatura numa assinatura de certificação. As transforms DocMDP e FieldMDP vão ambas para uma única matriz /Reference no valor da assinatura, cada uma com /Data a apontar para o catálogo
var
Options: TPadesSignOptions;
begin
Options := TPadesSignOptions.Default;
Options.CertificateThumbprint := 'A1B2C3D4E5F60718293A4B5C6D7E8F9012345678';
Options.Reason := 'Approved for release';
Options.FieldOptions.FieldName := 'Certification';
Options.FieldOptions.CertificationPermission := 2; // apenas preenchimento de formulário e assinatura
Options.FieldOptions.RequiredSeedValues := [psvcSubFilter, psvcDigestMethod];
Options.FieldOptions.LockAction := pfmaInclude; // bloqueia apenas estes campos
SetLength(Options.FieldOptions.LockFields, 2);
Options.FieldOptions.LockFields[0] := 'Total';
Options.FieldOptions.LockFields[1] := 'IBAN';
if not Pdf.SignPades('contract-certified.pdf', Options) then
Writeln('Signing failed');
end;
Alguns detalhes são fáceis de errar se fizer isto à mão. O /Perms /DocMDP do catálogo tem de referenciar o dicionário do valor da assinatura, não a anotação widget, e o escritor mantém o valor da assinatura como objeto indireto próprio por essa razão. Um dicionário /Perms existente pode já ter direitos de uso /UR3, pelo que o escritor copia-o e insere /DocMDP em vez de o substituir, seguindo o dicionário de permissões da ISO 32000-1 §12.8.4. Um documento que já transporte /DocMDP recusa uma segunda assinatura de certificação com EPadesCrypto, e o mesmo fazem opções inconsistentes: um lock Include ou Exclude sem nomes de campos, um lock All com uma lista de campos, um atestado legal numa assinatura não certificação, ou um ponto no nome do campo raiz. A assinatura remota acrescenta mais uma regra, porque o certificado de assinatura é desconhecido quando o PreparePadesRemoteSignature corre: definir aí CertificateRequired exige uma lista AcceptableCertificates explícita, enquanto a assinatura local pode recuar para o certificado do assinante resolvido
A análise de revisões completa a caixa de ferramentas de assinaturas em vez de substituir qualquer parte dela. Comece por inspecionar assinaturas PDF e níveis PAdES com o PDFium Component para ler o dicionário e o nível de base, veja porque é que os validadores recusam assinaturas PAdES para as falhas estruturais que vêm antes de qualquer pergunta de revisão, e funda o veredicto numa auditoria de riscos de segurança PDF mais ampla, ao lado das verificações de JavaScript e de ficheiros incorporados. O TPdf.AnalyzeSignatureRevisions, o TPadesSignatureFieldOptions e o escritor PAdES incremental aqui mostrado vêm com o PDFium Component para Delphi, C++Builder e Lazarus