Artículo técnico

FieldMDP en Delphi: detectar ediciones en campos bloqueados

PDFlibPas responde a la pregunta de FieldMDP a través de TPDFlib.GetSignatureModificationVerdict. Dado un PDF firmado, su contraseña y el nombre de un campo de firma, el método recorre cada actualización incremental anexada después de esa firma, resuelve el nombre completo de cada campo de formulario que esas actualizaciones tocaron, y devuelve 3 cuando se cambió un campo bloqueado por FieldMDP y 2 cuando solo se tocaron campos sin bloqueo. GetSignatureModificationDetail luego indica la revisión y el campo

El flujo de trabajo que necesita esto es el formulario de varios aprobadores. Un solicitante llena Order.Customer y Order.Total en una solicitud de compra y firma Approval.Requester, bloqueando esos dos campos. Un gerente agrega un centro de costo y firma Approval.Manager. Dos semanas después alguien abre el archivo en un editor de escritorio y sube Order.Total en una tercera actualización incremental. Todas las firmas siguen verificando, porque cada una cubre solo su propio rango de bytes. Las preguntas que un auditor realmente hace son qué campo cambió después de qué firma, y si ese cambio estaba permitido

¿Por qué DocMDP por sí solo no detecta un campo bloqueado editado?

DocMDP no puede detectarlo porque DocMDP clasifica tipos de cambio, no campos individuales. Los niveles de permiso de certificación en ISO 32000-1 §12.8.2.2 dicen si las actualizaciones posteriores pueden llenar formularios y firmar (P 2) o también anotar (P 3), y ese permiso se aplica por igual a todos los campos del documento. Editar Order.Total es llenar formularios, así que una certificación P 2 queda satisfecha justo con la edición que el solicitante quería prohibir. La clasificación de modificaciones a nivel de documento en PDFlibPas cubre esa capa, y este artículo baja un paso más fino

FieldMDP es la capa con granularidad de campo. Un campo de firma puede llevar un diccionario /Lock (ISO 32000-1 §12.7.4.5), y la firma que contiene puede llevar una transformación FieldMDP en su referencia de firma (ISO 32000-1 §12.8.2.4). Ambos usan el mismo vocabulario: /Action /All bloquea todos los campos, /Action /Include bloquea los nombres listados en /Fields, y /Action /Exclude bloquea todo excepto los nombres en /Fields. Antes de v3.586.0, GetSignatureModificationVerdict sabía que una revisión posterior había cambiado un campo de formulario, pero no cuál, así que cualquier documento con un bloqueo de campo cuyas actualizaciones posteriores iban más allá del material de validación volvía como 4, indeterminado. El mapa de revisiones ahora registra un nombre de campo resuelto para cada objeto relacionado con campos que clasifica, y eso es lo que convierte ese 4 en un 2 o un 3 reales

Escribir el bloqueo de campo cuando firma cada aprobador

TPDFlib.SetSignProcessFieldLock establece el bloqueo para la siguiente firma que un proceso de firma produzca. La acción 1 es All, 2 es Include, 3 es Exclude y 0 limpia el bloqueo; Include y Exclude toman una lista de nombres completos de campos separados por punto y coma. El campo firmado termina con un diccionario /Lock y una transformación FieldMDP que coincide, que es la pareja que los validadores estrictos esperan

Códigos de acción de PDFlibPas SetSignProcessFieldLock para firma FieldMDP en Delphi: la acción 1 All bloquea todos los campos de formulario incluidos los campos de firma posteriores, la acción 2 Include bloquea los campos de datos nombrados, la acción 3 Exclude bloquea todo excepto la lista, y la acción 0 limpia el bloqueo
Elija Include para los campos de datos que posee cada aprobador o Exclude listando los campos de firma posteriores; una llamada rechazada devuelve 0 y deja vigente el bloqueo anterior, así que siempre revise el 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: solo estos campos quedan congelados por esta firma
      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;

SetSignProcessFieldLock devuelve 0 en una llamada Include o Exclude sin nombres, y también en una lista que contenga una entrada vacía como 'Order.Customer;;Order.Total'; una llamada rechazada deja vigente el bloqueo configurado previamente, así que revise el valor de retorno en lugar de asumir que la última llamada ganó. El resto de la configuración de firma, elección de digest, reserva de /Contents y códigos de resultado, está cubierta en la guía de firma y validación PAdES para PDFlibPas. Un punto de diseño es específico de las cadenas de aprobación: un bloqueo /All en la firma del solicitante también congela Approval.Manager, porque un campo de firma también es un campo de formulario. Use Include para los campos de datos que posee cada aprobador, o Exclude listando los campos de firma posteriores

Ejecutar el veredicto a lo largo de una cadena de aprobación

GetSignatureModificationVerdict juzga una firma a la vez, así que una cadena de aprobación se revisa llamándolo una vez por campo de firma. El valor de retorno es un conjunto pequeño y cerrado, y cada valor significa algo distinto para un revisor:

  • 0: el archivo o el campo de firma indicado no se pudo leer
  • 1: no se anexó nada después de la firma
  • 2: solo se anexaron cambios que los permisos efectivos permiten, y ningún campo bloqueado fue tocado
  • 3: una revisión posterior hizo un cambio no permitido, incluida una edición en un campo que FieldMDP bloquea
  • 4: no se pudo determinar la respuesta, lo cual nunca debe leerse como aprobación
  • 5: la firma no termina en un límite de revisión, así que hay bytes dentro del rango que dice cubrir
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 getters describen la llamada recién hecha, así que léalos antes de la siguiente
      Log.Add(Format('%s: verdict %d, MDP level %d, %s',
        [Approvals[I], Verdict, Pdf.GetSignatureEffectiveMDPLevel,
         Pdf.GetSignatureModificationDetail]));
      case Verdict of
        1, 2: ;                                 // aceptado
        3: RejectForm(Approvals[I]);            // el detalle nombra campo y revisión
      else
        QueueForManualReview(Approvals[I]);     // 0, 4, 5
      end;
    end;
  finally
    Pdf.Free;
  end;
end;

Tome la solicitud de compra manipulada, donde la revisión 0 es el formulario en blanco, las revisiones 1 y 2 son las dos firmas y la revisión 3 es la edición. Como las definiciones de bloqueo se agrupan de todos los campos firmados, el veredicto para Approval.Manager ya aplica el bloqueo del solicitante, y su única actualización posterior es la propia edición. Cuando esa actualización reescribió solo el diccionario del campo, el veredicto es 3 y el detalle dice Revision 3 changed locked form field "Order.Total", which FieldMDP does not allow. Esa única línea responde las dos preguntas de auditoría. El veredicto para Approval.Requester tiene que pasar primero por la revisión de firma del gerente, y por las razones de la sección de limitaciones más abajo a menudo reportará 4, así que la firma más reciente suele dar la respuesta más nítida. GetSignatureEffectiveMDPLevel reporta el valor P de DocMDP más bajo encontrado entre las firmas, o 3 cuando nada certificó el documento, y ambos getters quedan válidos solo hasta la siguiente llamada de veredicto

Cronología de actualizaciones incrementales de un formulario PDF de Delphi manipulado leído por PDFlibPas GetSignatureModificationVerdict: la revisión 1 lleva un bloqueo Include sobre Order.Customer y Order.Total, la revisión 2 firma Approval.Manager, y la reescritura de la revisión 3 sobre el campo bloqueado Order.Total produce veredicto 3 para la firma del gerente y 4 para la firma del solicitante
Los bloqueos se agrupan de todos los campos firmados, así que el veredicto para Approval.Manager ya aplica el bloqueo del solicitante y la cadena de detalle nombra el campo y la revisión infractores, mientras que la firma más antigua a menudo cae en 4

¿Cómo sabe PDFlibPas qué campo cambió una revisión?

PDFlibPas reconstruye el nombre del campo desde el grafo de objetos de cada revisión, porque un objeto cambiado rara vez lleva su nombre completo. Un diccionario de campo con /FT o /V, o una anotación widget, guarda solo su nombre parcial en /T. El nombre completo descrito en ISO 32000-1 §12.7.3.2 es la cadena de nombres parciales desde la raíz, unidos con puntos, así que el mapa de revisiones sube por los enlaces /Parent y antepone cada /T que encuentra. Cada padre se lee de su definición más nueva en o antes de la revisión que se clasifica, lo que evita que un renombrado posterior de un nodo padre reescriba la historia de ediciones anteriores

Resolución del nombre completo FieldMDP en PDFlibPas para Delphi: el mapa de revisiones sube por los enlaces /Parent desde el widget cambiado, antepone cada nombre parcial /T unido con puntos para construir Order.Total, y lo compara contra las entradas /Fields por igualdad exacta o prefijo con punto, así un bloqueo sobre Order cubre cada descendiente
Un objeto cambiado rara vez lleva su nombre completo: los padres se leen de su definición más nueva en o antes de la revisión clasificada, los nombres se comparan distinguiendo mayúsculas, y el recorrido se detiene tras ocho niveles

Comparar el nombre resuelto contra los bloqueos es deliberadamente simple. Un nombre coincide con una entrada de /Fields cuando es igual a la entrada exactamente o empieza con la entrada seguida de un punto, así que bloquear Order también bloquea Order.Total y cualquier otro descendiente. All bloquea todos los nombres, Include bloquea los nombres que coinciden, y Exclude bloquea todos los nombres que no coinciden. Las definiciones de bloqueo provienen de todos los campos de firma firmados del archivo; cuando un campo tiene tanto un diccionario /Lock como una transformación FieldMDP, se usan los valores de /Lock. Para ver exactamente qué bloqueó cada firma, la clase de nivel más bajo TPDFlibSignDoc de la unidad PDFlibDigitalSign expone el reporte de permisos que el veredicto lee:

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;

Formularios cifrados: pase la contraseña o los nombres serán ilegibles

Un formulario cifrado tiene que descifrarse antes de poder resolver cualquier nombre de campo, así que GetSignatureModificationVerdict necesita el argumento Password para algo más que abrir el archivo. Cadenas como /T se cifran por objeto, y los campos escritos en flujos de objetos comprimidos no se pueden leer en absoluto hasta que el flujo que los contiene se descifre e infle. Desde v3.586.0 el mapa de revisiones interno acepta la contraseña, adjunta un contexto de descifrado mientras analiza flujos de objetos y diccionarios de revisión, y descifra cada objeto que decodifica, incluidas las versiones anteriores contra las que compara y los padres que recorre

El descifrado por objeto en el mapa de revisiones se activa solo cuando Password no está vacío. Un veredicto calculado sobre un archivo cifrado con una contraseña vacía debe tratarse como no verificado, incluso si el documento mismo abrió sin preguntar. Guarde la contraseña del formulario junto al registro del flujo de trabajo y pásela en cada llamada

Lo que el veredicto FieldMDP no cubre

La verificación de campos es una respuesta estructural a una pregunta muy acotada, y varios casos quedan fuera de ella. Conocerlos de antemano es lo que evita que un 4 se convierta en una escalada a soporte:

  • La comparación de campos corre solo después de que la verificación a nivel de documento haya salido limpia y al menos una revisión posterior haya cambiado más que material de validación o marcas de tiempo; una violación de DocMDP se reporta como 3 antes de mirar cualquier campo
  • Los objetos clasificados como relacionados con firmas pero sin nombre de campo, como el diccionario de valor de firma, un diccionario AcroForm reescrito, o un cambio de catálogo limitado a /AcroForm o /Perms, hacen que el veredicto devuelva 4 en lugar de adivinar, así que una revisión que agrega una firma de aprobación posterior bajo un bloqueo activo a menudo cae en 4
  • Solo los diccionarios de campo y las anotaciones widget reciben un nombre de campo; el mapa de revisiones no tiene una clase dedicada para flujos de apariencia, así que un editor que regenera apariencias junto con el valor puede ser detenido por la verificación a nivel de documento como un cambio de contenido antes de que corra la verificación de campos, y la cadena de detalle distingue los dos casos
  • El escaneo se detiene en el primer campo bloqueado o entrada sin nombre que encuentra, así que un 3 nombra un campo infractor, no todos
  • Los bloqueos se agrupan de todos los campos de firma firmados sin ordenarlos en el tiempo, así que un bloqueo escrito por un firmante posterior también se aplica a actualizaciones anteriores a él; esto solo puede empujar un resultado hacia 3, nunca hacia 2
  • Los nombres se comparan de forma exacta distinguiendo mayúsculas, y el recorrido de /Parent se detiene tras ocho niveles, así que una jerarquía inusualmente profunda resuelve a un nombre truncado
  • Una entrada FieldMDP cuya estructura no valida y cuya acción no se puede leer hace que el veredicto devuelva 4

Cuando un veredicto vuelve como 4, GetSignatureRevisionIndex da la revisión donde termina la firma y GetDocumentRevisionChanges lista cada objeto que cada revisión posterior agregó, modificó o liberó, con su clase de cambio. Eso basta para que una persona decida en un minuto. La mecánica de cómo se anexan esas revisiones está cubierta en la guía de actualizaciones incrementales para PDFlibPas

FAQ

¿Un veredicto de 2 significa que las firmas son válidas?

No. GetSignatureModificationVerdict examina qué se anexó después de una firma y nunca revisa el valor de firma CMS, la cadena de certificados ni la revocación. Un 2 dice que las actualizaciones posteriores se mantuvieron dentro de los permisos; la validación criptográfica es un paso aparte y se necesitan ambas

¿Por qué firmar un segundo aprobador produce 4 en lugar de 2?

Firmar anexa un diccionario de valor de firma y normalmente reescribe la contabilidad del formulario, como el diccionario AcroForm. Esos objetos no pertenecen a ningún campo con nombre, y cuando hay un bloqueo de campo activo el veredicto se rehúsa a dar por limpio un cambio que no puede atribuir a un campo. Use GetDocumentRevisionChanges para confirmar que la revisión contiene solo los objetos de firma esperados

¿Un solo nombre puede bloquear todo un grupo de campos?

Sí. Una entrada de bloqueo coincide con el campo nombrado y con cada descendiente cuyo nombre completo empiece con él seguido de un punto, así que listar Order en un bloqueo Include congela Order.Customer, Order.Total y cualquier otro campo bajo ese padre

Los veredictos FieldMDP, la firma con bloqueo de campos y la inspección de revisiones vienen todos en la PDFlibPas Delphi PDF Library, junto con las funciones de certificación DocMDP y validación de largo plazo PAdES de las que depende el flujo de aprobación de arriba