Artigo Técnico

FieldMDP no Delphi: Detetar Edições em Campos Bloqueados

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

Códigos de ação do SetSignProcessFieldLock do PDFlibPas para assinatura FieldMDP no Delphi: a ação 1 All bloqueia todos os campos de formulário incluindo os campos de assinatura posteriores, a ação 2 Include bloqueia os campos de dados nomeados, a ação 3 Exclude bloqueia tudo exceto a lista, e a ação 0 limpa o bloqueio
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 o bloqueio anterior de pé, por isso verifique sempre o valor devolvido
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

Linha temporal de atualizações incrementais de um formulário PDF Delphi adulterado lido pelo GetSignatureModificationVerdict do PDFlibPas: a revisão 1 transporta um bloqueio Include sobre Order.Customer e Order.Total, a revisão 2 assina Approval.Manager, e a reescritura da revisão 3 do campo bloqueado Order.Total produz veredicto 3 para a assinatura do gestor e 4 para a assinatura do requerente
Os bloqueios agregam-se de todos os campos assinados, pelo que o veredicto para o Approval.Manager já impõe o bloqueio do requerente e a string de detalhe nomeia o campo em causa e a revisão, enquanto a assinatura mais antiga frequentemente cai no 4

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

Resolução de nome totalmente qualificado FieldMDP no PDFlibPas para Delphi: o mapa de revisões sobe pelas ligações /Parent a partir do widget alterado, antepõe cada nome parcial /T unido por pontos para construir Order.Total, e compara-o com as entradas /Fields por igualdade exata ou prefixo com ponto, pelo que um bloqueio sobre Order cobre todos os descendentes
Um objeto alterado raramente transporta o seu nome completo: os pais são lidos da sua definição mais recente na revisão classificada ou anterior, os nomes comparam-se distinguindo maiúsculas de minúsculas, e a subida para após oito níveis

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