O PDFlibPas responde à pergunta do FieldMDP através do TPDFlib.GetSignatureModificationVerdict. Dado um PDF assinado, a sua palavra-passe e o nome de um campo de assinatura, o método percorre todas as atualizações incrementais acrescentadas depois dessa assinatura, resolve o nome totalmente qualificado de cada campo de formulário que essas atualizações tocaram, e devolve 3 quando um campo bloqueado pelo FieldMDP foi alterado e 2 quando só campos desbloqueados o foram. O GetSignatureModificationDetail depois nomeia a revisão e o campo
O fluxo de trabalho que precisa disto é o formulário de aprovação múltipla. Um requerente preenche Order.Customer e Order.Total num pedido de compra e assina Approval.Requester, bloqueando esses dois campos. Um gestor acrescenta um centro de custo e assina Approval.Manager. Duas semanas depois alguém abre o ficheiro num editor de desktop e empurra o Order.Total para cima numa terceira atualização incremental. Todas as assinaturas continuam a verificar, porque cada uma cobre apenas o seu próprio intervalo de bytes. As perguntas que um auditor realmente faz são qual campo mudou depois de qual assinatura, e se essa mudança era permitida
Por que o DocMDP sozinho não deteta um campo bloqueado editado?
O DocMDP não o consegue apanhar porque classifica tipos de alteração, não campos individuais. Os níveis de permissão de certificação na ISO 32000-1 §12.8.2.2 dizem se atualizações posteriores podem preencher formulários e assinar (P 2) ou também anotar (P 3), e essa permissão aplica-se igualmente a todos os campos do documento. Editar o Order.Total é preenchimento de formulário, pelo que uma certificação P 2 é satisfeita precisamente pela edição que o requerente pretendia proibir. A classificação de modificações ao nível do documento no PDFlibPas cobre essa camada, e este artigo desce até um nível mais fino
O FieldMDP é a camada com granularidade de campo. Um campo de assinatura pode transportar um dicionário /Lock (ISO 32000-1 §12.7.4.5), e a assinatura que contém pode transportar uma transformação FieldMDP na sua referência de assinatura (ISO 32000-1 §12.8.2.4). Ambos usam o mesmo vocabulário: /Action /All bloqueia todos os campos, /Action /Include bloqueia os nomes listados em /Fields, e /Action /Exclude bloqueia tudo exceto os nomes em /Fields. Antes da v3.586.0, o GetSignatureModificationVerdict sabia que uma revisão posterior tinha alterado um campo de formulário, mas não sabia qual, pelo que qualquer documento com um bloqueio de campo cujas atualizações posteriores iam além do material de validação regressava como 4, indeterminado. O mapa de revisões agora regista um nome de campo resolvido para cada objeto relacionado com campos que classifica, e é isso que transforma esse 4 num verdadeiro 2 ou 3
Escrever o bloqueio de campo quando cada aprovador assina
O TPDFlib.SetSignProcessFieldLock define o bloqueio para a próxima assinatura que um processo de assinatura produz. A ação 1 é All, 2 é Include, 3 é Exclude e 0 limpa o bloqueio; Include e Exclude recebem uma lista de nomes de campos totalmente qualificados separados por ponto e vírgula. O campo assinado acaba com um dicionário /Lock e uma transformação FieldMDP correspondente, que é o emparelhamento que os validadores estritos esperam
var
Pdf: TPDFlib;
SignId: Integer;
begin
Pdf := TPDFlib.Create;
try
SignId := Pdf.NewSignProcessFromFile('purchase-request.pdf', '');
if SignId = 0 then
raise Exception.Create('cannot open the form');
try
Pdf.SetSignProcessField(SignId, 'Approval.Requester');
Pdf.SetSignProcessPFXFromFile(SignId, 'requester.pfx', PfxPassword);
// 2 = Include: apenas estes campos ficam congelados por esta assinatura
if Pdf.SetSignProcessFieldLock(SignId, 2,
'Order.Customer;Order.Total') <> 1 then
raise Exception.Create('field lock rejected');
Pdf.EndSignProcessToFile(SignId, 'purchase-request-signed.pdf');
if Pdf.GetSignProcessResult(SignId) <> 1 then
raise Exception.CreateFmt('signing failed, code %d',
[Pdf.GetSignProcessResult(SignId)]);
finally
Pdf.ReleaseSignProcess(SignId);
end;
finally
Pdf.Free;
end;
end;
O SetSignProcessFieldLock devolve 0 numa chamada Include ou Exclude sem nomes, e também numa lista que contenha uma entrada vazia como 'Order.Customer;;Order.Total'; uma chamada rejeitada deixa o bloqueio anteriormente configurado no lugar, por isso verifique o valor devolvido em vez de presumir que a última chamada ganhou. O resto da configuração da assinatura, escolha do digest, reserva de /Contents e códigos de resultado, está coberto no guia de assinatura e validação PAdES para o PDFlibPas. Um ponto de design é específico das cadeias de aprovação: um bloqueio /All na assinatura do requerente também congela o Approval.Manager, porque um campo de assinatura também é um campo de formulário. Use Include para os campos de dados que cada aprovador possui, ou Exclude com os campos de assinatura posteriores listados
Executar o veredicto ao longo de uma cadeia de aprovação
O GetSignatureModificationVerdict julga uma assinatura de cada vez, pelo que uma cadeia de aprovação é verificada chamando-o uma vez por campo de assinatura. O valor devolvido é um conjunto pequeno e fechado, e cada valor significa algo diferente para um revisor:
- 0: o ficheiro ou o campo de assinatura nomeado não pôde ser lido
- 1: nada foi acrescentado depois da assinatura
- 2: apenas foram acrescentadas alterações que as permissões efetivas permitem, e nenhum campo bloqueado foi tocado
- 3: uma revisão posterior fez uma alteração que não é permitida, incluindo uma edição num campo que o FieldMDP bloqueia
- 4: a resposta não pôde ser determinada, o que nunca deve ser lido como aprovação
- 5: a assinatura não termina num limite de revisão, pelo que há bytes dentro do intervalo que afirma cobrir
const
Approvals: array[0..2] of WideString =
('Approval.Requester', 'Approval.Manager', 'Approval.Finance');
var
Pdf: TPDFlib;
I, Verdict: Integer;
begin
Pdf := TPDFlib.Create;
try
for I := Low(Approvals) to High(Approvals) do
begin
Verdict := Pdf.GetSignatureModificationVerdict(
'purchase-request-final.pdf', FormPassword, Approvals[I]);
// ambos os getters descrevem a chamada feita, por isso leia-os antes da seguinte
Log.Add(Format('%s: verdict %d, MDP level %d, %s',
[Approvals[I], Verdict, Pdf.GetSignatureEffectiveMDPLevel,
Pdf.GetSignatureModificationDetail]));
case Verdict of
1, 2: ; // aceite
3: RejectForm(Approvals[I]); // o detalhe nomeia campo e revisão
else
QueueForManualReview(Approvals[I]); // 0, 4, 5
end;
end;
finally
Pdf.Free;
end;
end;
Tome o pedido de compra adulterado, em que a revisão 0 é o formulário em branco, as revisões 1 e 2 são as duas assinaturas e a revisão 3 é a edição. Como as definições de bloqueio são agregadas de todos os campos assinados, o veredicto para o Approval.Manager já impõe o bloqueio do requerente, e a sua única atualização posterior é a própria edição. Quando essa atualização reescreveu apenas o dicionário do campo, o veredicto é 3 e o detalhe diz Revision 3 changed locked form field "Order.Total", which FieldMDP does not allow. Essa única linha responde às duas perguntas de auditoria. O veredicto para o Approval.Requester tem de passar primeiro pela assinatura da revisão do gestor, e pelas razões na secção de limitações abaixo frequentemente reportará 4, pelo que a assinatura mais recente costuma dar a resposta mais nítida. O GetSignatureEffectiveMDPLevel reporta o menor valor P de DocMDP encontrado entre as assinaturas, ou 3 quando nada certificou o documento, e ambos os getters permanecem válidos só até à próxima chamada de veredicto
Como sabe o PDFlibPas que campo uma revisão alterou?
O PDFlibPas reconstrói o nome do campo a partir do grafo de objetos de cada revisão, porque um objeto alterado raramente transporta o seu nome completo. Um dicionário de campo com /FT ou /V, ou uma anotação widget, guarda apenas o seu nome parcial em /T. O nome totalmente qualificado descrito na ISO 32000-1 §12.7.3.2 é a cadeia de nomes parciais desde a raiz, unidos por pontos, pelo que o mapa de revisões sobe pelas ligações /Parent e antepõe cada /T que encontra. Cada pai é lido da sua definição mais recente na revisão a classificar ou anterior, o que impede que uma renomeação posterior de um nó pai reescreva a história de edições anteriores
Comparar o nome resolvido com os bloqueios é deliberadamente simples. Um nome corresponde a uma entrada /Fields quando é igual à entrada exatamente ou começa pela entrada seguida de um ponto, pelo que bloquear Order também bloqueia Order.Total e todos os restantes descendentes. All bloqueia todos os nomes, Include bloqueia os nomes correspondentes, e Exclude bloqueia todos os nomes que não correspondem. As definições de bloqueio vêm de todos os campos de assinatura assinados no ficheiro; quando um campo tem tanto um dicionário /Lock como uma transformação FieldMDP, são usados os valores do /Lock. Para ver exatamente o que cada assinatura bloqueou, a classe de nível mais baixo TPDFlibSignDoc da unidade PDFlibDigitalSign expõe o relatório de permissões que o veredicto lê:
uses
PDFlibDigitalSign;
var
SignDoc: TPDFlibSignDoc;
Report: TPDFlibSignaturePermissionReport;
Names: TStringList;
I, J: Integer;
Line: string;
begin
SignDoc := TPDFlibSignDoc.Create;
Names := TStringList.Create;
try
if not SignDoc.Open('purchase-request-final.pdf', FormPassword, False) then
Exit;
try
SignDoc.GetSignatureFieldNames(Names);
for I := 0 to Names.Count - 1 do
begin
SignDoc.GetSignaturePermissionInfoByName(Names[I], Report);
if not (Report.HasFieldLock or Report.HasFieldMDP) then
Continue;
Line := Names[I] + ': ' + string(Report.FieldLockAction);
for J := 0 to High(Report.FieldNames) do
Line := Line + ' ' + string(Report.FieldNames[J]);
if Report.HasFieldLock and Report.HasFieldMDP and
not Report.FieldMDPMatches then
Line := Line + ' (lock and FieldMDP transform disagree)';
Log.Add(Line);
end;
finally
SignDoc.Close;
end;
finally
Names.Free;
SignDoc.Free;
end;
end;
Formulários encriptados: passe a palavra-passe ou os nomes são ilegíveis
Um formulário encriptado tem de ser desencriptado antes de qualquer nome de campo poder ser resolvido, pelo que o GetSignatureModificationVerdict precisa do argumento Password para mais do que abrir o ficheiro. Strings como /T são encriptadas por objeto, e campos escritos em fluxos de objetos comprimidos não podem ser lidos de todo até que o fluxo que os contém seja desencriptado e descomprimido. Desde a v3.586.0 que o mapa de revisões interno aceita a palavra-passe, anexa um contexto de desencriptação enquanto analisa fluxos de objetos e dicionários de revisão, e desencripta cada objeto que descodifica, incluindo as versões anteriores com que faz diffs e os pais que percorre
A desencriptação por objeto no mapa de revisões só é ativada quando Password não está vazia. Um veredicto calculado sobre um ficheiro encriptado com uma palavra-passe vazia deve por isso ser tratado como não verificado, mesmo que o documento em si tenha aberto sem pedir palavra-passe. Guarde a palavra-passe do formulário junto ao registo do fluxo de trabalho e passe-a em cada chamada
O que o veredicto FieldMDP não cobre
A verificação de campos é uma resposta estrutural a uma pergunta estreita, e vários casos ficam fora dela. Conhecê-los com antecedência é o que impede que um 4 se transforme numa escalada para o suporte:
- A comparação de campos corre apenas depois de a verificação ao nível do documento ter saído limpa e de pelo menos uma revisão posterior ter alterado mais do que material de validação ou selos de tempo; uma violação de DocMDP é reportada como 3 antes de qualquer campo ser examinado
- Objetos classificados como relacionados com assinaturas mas sem nome de campo, como o dicionário de valor de assinatura, um dicionário AcroForm reescrito, ou uma alteração de catálogo limitada a
/AcroFormou/Perms, fazem o veredicto devolver 4 em vez de adivinhar, pelo que uma revisão que acrescente uma assinatura de aprovação posterior sob um bloqueio ativo frequentemente cai no 4 - Só dicionários de campo e anotações widget recebem um nome de campo; o mapa de revisões não tem classe dedicada para fluxos de aparência, pelo que um editor que regenere aparências juntamente com o valor pode ser travado pela verificação ao nível do documento como alteração de conteúdo antes de a verificação de campos correr, e a string de detalhe distingue os dois casos
- A varredura para na primeira entrada bloqueada ou sem nome que encontra, pelo que um 3 nomeia um campo em causa, não todos
- Os bloqueios agregam-se de todos os campos de assinatura assinados sem os ordenar no tempo, pelo que um bloqueio escrito por um assinante posterior também se aplica a atualizações anteriores a ele; isto só pode empurrar um resultado para 3, nunca para 2
- Os nomes comparam-se exatamente e distinguindo maiúsculas de minúsculas, e a subida por
/Parentpara após oito níveis, pelo que uma hierarquia invulgarmente profunda resolve para um nome truncado - Uma entrada FieldMDP cuja estrutura não valida e cuja ação não pode ser lida faz o veredicto devolver 4
Quando um veredicto regressa como 4, o GetSignatureRevisionIndex dá a revisão onde a assinatura termina e o GetDocumentRevisionChanges lista cada objeto que cada revisão posterior acrescentou, modificou ou libertou, com a sua classe de alteração. Isso chega para uma pessoa decidir num minuto. A mecânica de como essas revisões são acrescentadas está coberta no guia de atualização incremental para o PDFlibPas
FAQ
Um veredicto de 2 significa que as assinaturas são válidas?
Não. O GetSignatureModificationVerdict examina o que foi acrescentado depois de uma assinatura e nunca verifica o valor da assinatura CMS, a cadeia de certificados ou a revogação. Um 2 diz que as atualizações posteriores se mantiveram dentro das permissões; a validação criptográfica é um passo separado e ambos são necessários
Por que um segundo aprovador a assinar produz 4 em vez de 2?
Assinar acrescenta um dicionário de valor de assinatura e normalmente reescreve a contabilidade interna do formulário, como o dicionário AcroForm. Esses objetos não pertencem a nenhum campo nomeado, e quando um bloqueio de campo está ativo o veredicto recusa-se a dar como limpa uma alteração que não consegue atribuir a um campo. Use o GetDocumentRevisionChanges para confirmar que a revisão contém apenas os objetos de assinatura esperados
Um único nome pode bloquear um grupo inteiro de campos?
Sim. Uma entrada de bloqueio corresponde ao campo nomeado e a todos os descendentes cujo nome totalmente qualificado começa por ele seguido de um ponto, pelo que listar Order num bloqueio Include congela o Order.Customer, o Order.Total e qualquer outro campo sob esse pai
Veredictos FieldMDP, assinatura com bloqueio de campo e inspeção de revisões viajam todos no PDFlibPas Delphi PDF Library, juntamente com as funcionalidades de certificação DocMDP e validação de longo prazo PAdES de que o fluxo de aprovação acima depende