O PDFlibPas responde à pergunta do FieldMDP por meio de TPDFlib.GetSignatureModificationVerdict. Dado um PDF assinado, sua senha e o nome de um campo de assinatura, o método percorre cada atualização incremental acrescentada 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 travado pelo FieldMDP foi alterado e 2 quando apenas campos destravados foram. O GetSignatureModificationDetail então informa a revisão e o campo
O fluxo de trabalho que precisa disso é o formulário de múltiplos aprovadores. Um solicitante preenche Order.Customer e Order.Total em uma requisição de compra e assina Approval.Requester, travando esses dois campos. Um gerente acrescenta um centro de custo e assina Approval.Manager. Duas semanas depois alguém abre o arquivo em um editor de desktop e empurra o Order.Total para cima em uma terceira atualização incremental. Toda assinatura continua validando, porque cada uma cobre apenas o próprio intervalo de bytes. As perguntas que um auditor realmente faz são qual campo mudou depois de qual assinatura, e se aquela mudança era permitida
Por que o DocMDP sozinho não detecta um campo bloqueado editado?
O DocMDP não detecta porque classifica tipos de mudança, não campos individuais. Os níveis de permissão de certificação da 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 vale igualmente para todo campo do documento. Editar o Order.Total é preenchimento de formulário, então uma certificação P 2 é satisfeita justamente pela edição que o solicitante queria proibir. A classificação de modificação em nível de documento no PDFlibPas cobre essa camada, e este artigo dá um passo a mais na granularidade
O FieldMDP é a camada com granularidade de campo. Um campo de assinatura pode carregar um dicionário /Lock (ISO 32000-1 §12.7.4.5), e a assinatura que ele contém pode carregar uma transformação FieldMDP em sua referência de assinatura (ISO 32000-1 §12.8.2.4). Ambos usam o mesmo vocabulário: /Action /All trava todos os campos, /Action /Include trava os nomes listados em /Fields, e /Action /Exclude trava tudo exceto os nomes em /Fields. Antes da v3.586.0, o GetSignatureModificationVerdict sabia que uma revisão posterior havia alterado um campo de formulário, mas não sabia qual, então qualquer documento com trava de campo cujas atualizações posteriores fossem além do material de validação voltava como 4, indeterminado. O mapa de revisões agora registra um nome de campo resolvido para cada objeto relacionado a campo que classifica, e é isso que transforma aquele 4 em um 2 ou 3 real
Gravando a trava de campo quando cada aprovador assina
O TPDFlib.SetSignProcessFieldLock define a trava para a próxima assinatura que um processo de assinatura produz. Ação 1 é All, 2 é Include, 3 é Exclude e 0 limpa a trava; 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 par que validadores rigorosos 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 para uma chamada Include ou Exclude sem nomes, e também para uma lista com entrada vazia, como 'Order.Customer;;Order.Total'; uma chamada rejeitada mantém a trava configurada anteriormente, então confira o valor de retorno em vez de presumir que a última chamada venceu. O restante da configuração de assinatura, escolha de digest, reserva de /Contents e códigos de resultado está coberto no guia de assinatura e validação PAdES do PDFlibPas. Um ponto de projeto é específico de cadeias de aprovação: uma trava /All na assinatura do solicitante 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
Executando o veredito ao longo de uma cadeia de aprovação
O GetSignatureModificationVerdict julga uma assinatura por vez, então uma cadeia de aprovação é verificada chamando-o uma vez por campo de assinatura. O valor de retorno é um conjunto pequeno e fechado, e cada valor significa algo diferente para um revisor:
- 0: o arquivo ou o campo de assinatura nomeado não pôde ser lido
- 1: nada foi acrescentado depois da assinatura
- 2: apenas mudanças que as permissões efetivas permitem foram acrescentadas, e nenhum campo travado foi tocado
- 3: uma revisão posterior fez uma mudança que não é permitida, incluindo uma edição em um campo que o FieldMDP trava
- 4: a resposta não pôde ser determinada, o que jamais deve ser lido como aprovação
- 5: a assinatura não termina em uma fronteira de revisão, então bytes ficam dentro do intervalo que ela declara 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 recém-feita, então leia-os antes da próxima
Log.Add(Format('%s: verdict %d, MDP level %d, %s',
[Approvals[I], Verdict, Pdf.GetSignatureEffectiveMDPLevel,
Pdf.GetSignatureModificationDetail]));
case Verdict of
1, 2: ; // aceito
3: RejectForm(Approvals[I]); // o detalhe informa campo e revisão
else
QueueForManualReview(Approvals[I]); // 0, 4, 5
end;
end;
finally
Pdf.Free;
end;
end;
Pegue a requisição de compra adulterada, 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 trava são reunidas de todo campo assinado, o veredito para o Approval.Manager já aplica a trava do solicitante, e sua única atualização posterior é a própria edição. Quando essa atualização regravou apenas o dicionário do campo, o veredito é 3 e o detalhe traz Revision 3 changed locked form field "Order.Total", which FieldMDP does not allow. Essa única linha responde as duas perguntas de auditoria. O veredito para o Approval.Requester precisa passar primeiro pela revisão de assinatura do gerente, e pelos motivos da seção de limitações abaixo ele frequentemente reporta 4, então a assinatura mais recente costuma dar a resposta mais precisa. 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 apenas até a próxima chamada de veredito
Como o PDFlibPas sabe qual 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 carrega seu nome completo. Um dicionário de campo com /FT ou /V, ou uma anotação widget, guarda apenas seu nome parcial em /T. O nome totalmente qualificado descrito na ISO 32000-1 §12.7.3.2 é a cadeia de nomes parciais a partir da raiz, unidos por pontos, então o mapa de revisões sobe pelos links /Parent e antepõe cada /T que encontra. Cada pai é lido de sua definição mais recente na revisão sendo classificada ou antes dela, o que impede que uma renomeação posterior de um nó pai reescreva o histórico de edições anteriores
Comparar o nome resolvido com as travas é propositalmente simples. Um nome casa com uma entrada /Fields quando é igual à entrada exatamente ou começa com a entrada seguida de ponto, então travar o Order também trava o Order.Total e todo outro descendente. All trava todo nome, Include trava os nomes que casam, e Exclude trava todo nome que não casa. As definições de trava vêm de todo campo de assinatura assinado no arquivo; quando um campo tem tanto um dicionário /Lock quanto uma transformação FieldMDP, os valores do /Lock são usados. Para ver exatamente o que cada assinatura travou, a classe TPDFlibSignDoc, de nível mais baixo, da unit PDFlibDigitalSign expõe o relatório de permissões que o veredito 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 criptografados: passe a senha ou os nomes ficam ilegíveis
Um formulário criptografado precisa ser decifrado antes que qualquer nome de campo possa ser resolvido, então o GetSignatureModificationVerdict precisa do argumento Password para mais do que abrir o arquivo. Strings como /T são criptografadas por objeto, e campos gravados em object streams comprimidos não podem ser lidos de forma alguma até que o stream que os contém seja decifrado e descomprimido. Desde a v3.586.0 o mapa de revisões interno aceita a senha, anexa um contexto de decifragem enquanto analisa object streams e dicionários de revisão, e decifra cada objeto que decodifica, incluindo as versões anteriores com que faz diff e os pais que percorre
A decifragem por objeto no mapa de revisões só é ativada quando Password não está vazia. Um veredito calculado em um arquivo criptografado com senha vazia deve, portanto, ser tratado como não verificado, mesmo que o próprio documento tenha aberto sem pedir senha. Guarde a senha do formulário junto ao registro do fluxo de trabalho e passe-a em toda chamada
O que o veredito do FieldMDP não cobre
A verificação de campo é 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 vire uma escalada de suporte:
- A comparação de campos roda apenas depois que a verificação em nível de documento saiu limpa e ao menos uma revisão posterior mudou mais que material de validação ou carimbos de tempo; uma violação de DocMDP é reportada como 3 antes que qualquer campo seja olhado
- Objetos classificados como relacionados a assinatura mas sem nome de campo, como o dicionário de valor de assinatura, um dicionário AcroForm regravado, ou uma mudança de catálogo limitada a
/AcroFormou/Perms, fazem o veredito devolver 4 em vez de adivinhar, então uma revisão que acrescenta uma assinatura de aprovação posterior sob trava ativa frequentemente cai no 4 - Apenas dicionários de campo e anotações widget recebem nome de campo; o mapa de revisões não tem classe dedicada para appearance streams, então um editor que regenera aparências junto com o valor pode ser barrado pela verificação em nível de documento como mudança de conteúdo antes que a verificação de campo rode, e a string de detalhe distingue os dois casos
- A varredura para no primeiro campo travado ou entrada sem nome que encontra, então um 3 informa um campo infrator, não todos
- As travas são reunidas de todo campo de assinatura assinado sem ordená-las no tempo, então uma trava escrita por um assinante posterior também é aplicada a atualizações que vieram antes dela; isso só pode empurrar um resultado para 3, nunca para 2
- Os nomes são comparados de forma exata e diferenciando maiúsculas, e a subida pelos
/Parentpara após oito níveis, então uma hierarquia incomumente 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 veredito devolver 4
Quando um veredito volta como 4, o GetSignatureRevisionIndex dá a revisão em que a assinatura termina e o GetDocumentRevisionChanges lista todo objeto que cada revisão posterior acrescentou, modificou ou liberou, com sua classe de mudança. Isso basta para uma pessoa decidir em um minuto. A mecânica de como essas revisões são acrescentadas está coberta no guia de atualização incremental do PDFlibPas
FAQ
Um veredito 2 significa que as assinaturas são válidas?
Não. O GetSignatureModificationVerdict examina o que foi acrescentado depois de uma assinatura e nunca confere o valor de assinatura CMS, a cadeia de certificados ou revogação. Um 2 diz que as atualizações posteriores ficaram dentro das permissões; validação criptográfica é um passo separado e ambos são necessários
Por que a assinatura de um segundo aprovador produz 4 em vez de 2?
Assinar acrescenta um dicionário de valor de assinatura e geralmente regrava a contabilidade do formulário, como o dicionário AcroForm. Esses objetos não pertencem a nenhum campo nomeado, e quando uma trava de campo está ativa o veredito recusa a dar por boa uma mudança 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 travar um grupo inteiro de campos?
Sim. Uma entrada de trava casa com o campo nomeado e todo descendente cujo nome totalmente qualificado começa com ele seguido de ponto, então listar Order em uma trava Include congela o Order.Customer, o Order.Total e qualquer outro campo sob aquele pai
Vereditos de FieldMDP, assinatura com trava de campo e inspeção de revisões vêm todos no PDFlibPas Delphi PDF Library, junto com os recursos de certificação DocMDP e validação de longo prazo PAdES de que o fluxo de aprovação acima depende