Artigo Técnico

FieldMDP em Delphi: detectando edições em campos bloqueados

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

Códigos de ação de PDFlibPas SetSignProcessFieldLock para assinatura FieldMDP em Delphi: ação 1 All trava todo campo de formulário incluindo campos de assinatura posteriores, ação 2 Include trava os campos de dados nomeados, ação 3 Exclude trava tudo exceto a lista, e ação 0 limpa a trava
Escolha Include para os campos de dados que cada aprovador possui ou Exclude com os campos de assinatura posteriores listados; uma chamada rejeitada devolve 0 e deixa a trava anterior de pé, então sempre confira o valor de retorno
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

Linha do tempo de atualização incremental de um formulário PDF Delphi adulterado lido por PDFlibPas GetSignatureModificationVerdict: a revisão 1 carrega uma trava Include sobre Order.Customer e Order.Total, a revisão 2 assina Approval.Manager, e a regravação da revisão 3 no campo Order.Total travado produz veredito 3 para a assinatura do gerente e 4 para a do solicitante
As travas são reunidas de todo campo assinado, então o veredito para Approval.Manager já aplica a trava do solicitante e a string de detalhe informa o campo e a revisão infratores, enquanto a assinatura mais antiga frequentemente cai no 4

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

Resolução de nome totalmente qualificado do FieldMDP no PDFlibPas para Delphi: o mapa de revisões sobe pelos links /Parent a partir do widget alterado, antepõe cada nome parcial /T unido por pontos para montar Order.Total, e o compara com as entradas /Fields por igualdade exata ou prefixo com ponto, de modo que uma trava sobre Order cubra todo descendente
Um objeto alterado raramente carrega seu nome completo: os pais são lidos de sua definição mais recente na revisão classificada ou antes dela, os nomes são comparados diferenciando maiúsculas, e a subida para após oito níveis

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 /AcroForm ou /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 /Parent para 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