PDFlibPas responde a la pregunta de FieldMDP mediante 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 añadida 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 bloquear. GetSignatureModificationDetail nombra además la revisión y el campo
El flujo que necesita esto es el formulario con varios aprobadores. Un solicitante rellena Order.Customer y Order.Total en una solicitud de compra y firma Approval.Requester, bloqueando esos dos campos. Un responsable añade un centro de coste y firma Approval.Manager. Dos semanas después alguien abre el fichero 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 hace de verdad son qué campo cambió después de qué firma, y si ese cambio estaba permitido
Por qué DocMDP 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 de ISO 32000-1 §12.8.2.2 dicen si las actualizaciones posteriores pueden rellenar 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 relleno de formularios, así que una certificación P 2 queda satisfecha exactamente por 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 signature reference (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 de /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á de material de validación volvía como 4, indeterminado. El mapa de revisiones registra ahora 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 fija el bloqueo para la siguiente firma que produzca un proceso de firma. 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 acaba con un diccionario /Lock y una transformación FieldMDP que casa, que es la pareja que esperan los validadores estrictos
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 para una llamada Include o Exclude sin nombres, y también para una lista con una entrada vacía como 'Order.Customer;;Order.Total'; una llamada rechazada deja el bloqueo configurado previamente en su sitio, así que comprobad el valor devuelto 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á cubierto en la guía de firma y validación PAdES de 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. Usad Include para los campos de datos que cada aprobador posee, o Exclude con los campos de firma posteriores listados
Ejecutar el veredicto a lo largo de una cadena de aprobación
GetSignatureModificationVerdict juzga una firma cada vez, así que una cadena de aprobación se comprueba llamándolo una vez por campo de firma. El valor devuelto es un conjunto pequeño y cerrado, y cada valor significa algo distinto para quien revisa:
- 0: el fichero o el campo de firma nombrado no se pudieron leer
- 1: no se añadió nada después de la firma
- 2: solo se añadieron cambios que los permisos vigentes permiten, y no se tocó ningún campo bloqueado
- 3: una revisión posterior hizo un cambio no permitido, incluida una edición de un campo que FieldMDP bloquea
- 4: la respuesta no se pudo determinar, algo que 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 leedlos 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;
Tomad 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 responsable, y por los motivos de la sección de limitaciones de abajo a menudo informará 4, así que la firma más reciente suele dar la respuesta más nítida. GetSignatureEffectiveMDPLevel informa del valor P de DocMDP más bajo encontrado entre las firmas, o 3 cuando nada certificó el documento, y ambos getters siguen siendo válidos solo hasta la siguiente llamada de veredicto
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 widget annotation, solo contiene 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 recorre los enlaces /Parent hacia arriba y antepone cada /T que encuentra. Cada padre se lee de su definición más reciente 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
Comparar el nombre resuelto con los bloqueos es deliberadamente simple. Un nombre casa con una entrada de /Fields cuando es igual a la entrada exactamente o empieza por la entrada seguida de un punto, así que bloquear Order bloquea también Order.Total y cualquier otro descendiente. All bloquea todos los nombres, Include bloquea los nombres que casan, y Exclude bloquea todos los nombres que no casan. Las definiciones de bloqueo vienen de todos los campos de firma firmados del fichero; cuando un campo tiene a la vez un diccionario /Lock y una transformación FieldMDP, se usan los valores de /Lock. Para ver exactamente qué bloqueó cada firma, la clase de nivel inferior TPDFlibSignDoc de la unidad PDFlibDigitalSign expone el informe de permisos que lee el veredicto:
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: pasad la contraseña o los nombres son 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 fichero. Cadenas como /T se cifran por objeto, y los campos escritos en object streams comprimidos no se pueden leer en absoluto hasta que el stream contenedor se descifra e infla. Desde v3.586.0 el mapa de revisiones interno acepta la contraseña, adjunta un contexto de descifrado mientras analiza object streams 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 del mapa de revisiones solo se activa cuando Password no está vacía. Un veredicto calculado sobre un fichero cifrado con una contraseña vacía debe tratarse por tanto como no verificado, aunque el documento en sí se abriera sin preguntar. Guardad la contraseña del formulario junto al registro del flujo y pasadla en cada llamada
Lo que el veredicto FieldMDP no cubre
La comprobación de campos es una respuesta estructural a una pregunta estrecha, y varios casos quedan fuera de ella. Conocerlos por adelantado es lo que evita que un 4 se convierta en una escalada de soporte:
- La comparación de campos corre solo después de que la comprobación a nivel de documento haya salido limpia y al menos una revisión posterior haya cambiado algo más que material de validación o sellos de tiempo; una violación de DocMDP se informa 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
/AcroFormo/Perms, hacen que el veredicto devuelva 4 en lugar de adivinar, así que una revisión que añade una firma de aprobación posterior bajo un bloqueo activo suele quedarse en 4 - Solo los diccionarios de campo y las widget annotations reciben nombre de campo; el mapa de revisiones no tiene clase dedicada para appearance streams, así que un editor que regenera appearances junto con el valor puede ser detenido por la comprobación a nivel de documento como cambio de contenido antes de que corra la comprobació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 y distinguiendo mayúsculas, y el recorrido de
/Parentse detiene tras ocho niveles, así que una jerarquía inusualmente profunda se 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 añadió, modificó o liberó, con su clase de cambio. Es suficiente para que una persona decida en un minuto. La mecánica de cómo se añaden esas revisiones está cubierta en la guía de actualización incremental de PDFlibPas
FAQ
¿Un veredicto de 2 significa que las firmas son válidas?
No. GetSignatureModificationVerdict examina qué se añadió después de una firma y nunca comprueba 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 hacen falta ambos
¿Por qué la firma de un segundo aprobador produce 4 en lugar de 2?
Firmar añade un diccionario de valor de firma y normalmente reescribe contabilidad de formularios como el diccionario AcroForm. Esos objetos no pertenecen a ningún campo con nombre, y cuando un bloqueo de campo está activo el veredicto se niega a dar por buena un cambio que no puede atribuir a un campo. Usad GetDocumentRevisionChanges para confirmar que la revisión contiene solo los objetos de firma esperados
¿Puede un solo nombre bloquear todo un grupo de campos?
Sí. Una entrada de bloqueo casa con el campo nombrado y todos los descendientes cuyo nombre completo empieza por é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 campo 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 a largo plazo PAdES de las que depende el flujo de aprobación de arriba