Un PDF firmado que cambió después de la firma no está automáticamente roto. La norma ISO 32000-1 permite actualizaciones incrementales sobre una firma, y solo algunas de ellas rompen la política que estableció el firmante. HotPDF Component para Delphi y C++Builder responde a esa pregunta con AnalyzeLoadedSignatureRevisions, que clasifica cada revisión posterior a la firma y la evalúa frente a DocMDP y FieldMDP. El escenario resulta familiar a cualquiera que distribuya software de contratos: tu cliente firma un acuerdo de compra, lo envía, y lo recibe de vuelta con una página de anexo adjunta. El lector muestra una barra amarilla que indica que la firma está intacta pero que el documento se ha modificado desde que se firmó, y nadie en la sala puede decir si eso es un flujo normal de contrafirma o alguien editando en silencio un contrato firmado
¿Qué cuenta como un cambio legítimo tras la firma?
Un cambio es legítimo cuando su categoría semántica cae dentro del permiso que declaró la firma certificadora. La norma ISO 32000-1 §12.8.2.2 define la transformación DocMDP con un valor /P de 1, 2 o 3: 1 no permite ningún cambio, 2 permite rellenar formularios y firmar, 3 permite rellenar formularios, firmar y añadir anotaciones. HotPDF expone eso como los valores THPDFDocMDPPermission dmpNoChanges, dmpFormFillAndSign y dmpFormFillSignAndAnnotate, con dmpNone reservado para resultados de inspección que no llevan ninguna transformación DocMDP
Las categorías están ordenadas, y ese orden es el motor de toda la comprobación. THPDFRevisionModificationLevel recorre rmlNone, rmlLongTermValidation, rmlFormFillAndSign, rmlAnnotations, rmlOther, dispuestos deliberadamente de forma que un ordinal mayor nunca sea menos restrictivo. Un documento completo se reduce al nivel máximo observado en todas las revisiones posteriores a la firma, y la comparación con DocMDP se convierte en una única prueba de enteros. Un matiz importa desde el principio: en dmpNoChanges el análisis sigue aceptando rmlLongTermValidation. Añadir material de validación DSS y VRI o un sello de tiempo de documento a un archivo certificado es mantenimiento de la firma, no modificación del documento, y tratarlo como una infracción rompería todos los flujos de archivado a largo plazo existentes
¿Cómo reconstruye HotPDF la cadena de revisiones?
De forma estructural, no heurística. Según la norma ISO 32000-1 §7.5.6, una actualización incremental añade una nueva sección de referencias cruzadas cuyo /Prev apunta a la anterior, así que HotPDF lee startxref desde el final, analiza la sección que hay ahí, sigue /Prev hacia atrás y repite, devolviendo las secciones de la más antigua a la más reciente. En ese bucle hay dos límites de seguridad, y ambos merece la pena conocerlos al triar un archivo que falla: un /Prev que apunta a un desplazamiento ya visitado termina el recorrido con un diagnóstico explícito de ciclo en lugar de quedarse en bucle, y una cadena de más de mil revisiones se rechaza directamente. Ambos casos aparecen en Analysis.Issue con la función devolviendo False, y ninguno debería taparse, porque un /Prev cíclico es un archivo malformado u hostil, no simplemente inusual
En documentos reales aparecen cuatro formas históricas, y las cuatro están cubiertas: las tablas xref tradicionales analizadas línea a línea, los flujos de referencias cruzadas descomprimidos y decodificados mediante sus campos /W e /Index, los archivos de referencia híbrida cuyo trailer tradicional lleva una clave /XRefStm que se analiza y se fusiona en la misma revisión (el caso del productor de Office, tratado en el artículo sobre flujos de referencias cruzadas híbridos), y los objetos que viven dentro de un contenedor ObjStm, que importan porque una actualización moderna suele poner el diccionario cambiado en un flujo comprimido en lugar de escribirlo directamente, como se describe en el artículo sobre flujos de objetos y actualizaciones incrementales. La firma ancla la división: /ByteRange[2] + /ByteRange[3] se convierte en SignedRevisionLength, y toda sección en ese desplazamiento o más allá es posterior a la firma. Si el rango de bytes sigue produciendo el mismo hash es una pregunta distinta, respondida por VerifyLoadedSignature y tratada en el artículo sobre verificación de firmas digitales PDF
Cómo se clasifica cada objeto cambiado
La clasificación se ejecuta por objeto y luego se propaga por las referencias. Para cada número de objeto que toca una sección posterior a la firma, HotPDF lee el nuevo cuerpo y el cuerpo tal y como estaba en la instantánea firmada; un cuerpo idéntico es rmlNone, porque los productores sí reescriben objetos sin cambiarlos. Los reconocedores son deliberadamente estrechos. Un objeto /Type /DocTimeStamp, o uno cuyo /SubFilter sea ETSI.RFC3161, es rmlLongTermValidation, igual que cualquier cosa alcanzable desde el árbol /DSS del catálogo; un diccionario /Type /Sig es rmlFormFillAndSign. Para los contenedores, la prueba es qué claves se movieron, no qué es el objeto: el catálogo solo puede ganar o alterar /DSS, /Extensions o /AcroForm; el diccionario AcroForm solo /Fields, /SigFlags, /NeedAppearances, /DR, /DA o /Q; una página solo /Annots; un campo o widget solo /V, /AP, /AS o /M. Cualquier cosa fuera de esos conjuntos cae en rmlOther, que es exactamente cómo se detecta la página de anexo añadida: agregar una página reorganiza el árbol de páginas de formas que ninguna lista blanca cubre, y ningún relleno de formulario legítimo se parece a eso
Después los niveles se propagan, y cada contenedor hereda el nivel máximo de los hijos cambiados a los que apunta, iterando hasta que la asignación se estabiliza. Esto es lo que hace que funcionen los flujos de apariencia. Un campo de texto rellenado reescribe /V y apunta a un flujo /AP nuevo, y ese flujo por sí solo es un blob anónimo de operadores de contenido sin ningún tipo que reconocer; como el campo que lo posee es rmlFormFillAndSign, el flujo hereda el mismo nivel en lugar de caer en rmlOther. La misma propagación traslada el contexto DSS a los flujos de certificado y revocación que de otro modo serían inclasificables
¿Por qué un objeto ilegible cuenta como infracción?
Porque la alternativa es un validador derrotado escribiendo algo que no entiende. En HotPDF, tres situaciones terminan en rmlOther sin posibilidad de apelación: un objeto cuyo cuerpo no se pudo leer de la revisión, un objeto que la revisión marca como liberado, y un objeto que no coincide con ninguno de los reconocedores anteriores. Cada uno registra un diagnóstico específico en el campo Issue de la revisión, de modo que un operador pueda ver qué número de objeto produjo el veredicto
Liberar es la más tajante de las tres. Una revisión posterior a la firma que marca como libre un objeto previamente definido ha eliminado contenido de un documento firmado, y ningún nivel de permiso bajo el §12.8.2.2 lo permite; los números de objeto aterrizan en FreedObjectNumbers y la revisión se eleva a rmlOther. Los objetos ilegibles siguen la misma lógica por un motivo distinto. Un validador que no puede analizar un objeto no tiene base para declararlo inofensivo, y la respuesta honesta a eso no es el silencio. Informar de una construcción inusual pero benigna como infracción cuesta una revisión humana; el error contrario envía un contrato firmado con una edición no advertida en su interior
Leer el veredicto en Delphi
La llamada es breve. Carga el documento, elige un índice de firma, lee el registro; la sobrecarga sin parámetros reabre el archivo del que se cargó el documento, y la sobrecarga con TStream toma bytes suministrados por el llamador y restaura la posición del flujo antes de devolver el control. PolicyCompliant es el booleano único que la mayoría de llamadores quiere, combinando tres decisiones independientes: la validez estructural de los diccionarios de permiso, DocMDPCompliant y FieldMDPCompliant. Mantén los componentes visibles en tu interfaz en lugar de colapsarlos, y ten en cuenta que un documento sin transformación DocMDP deja DocMDPCompliant en True, ya que una firma de aprobación ordinaria no declara ninguna política que infringir, y el ModificationLevel agregado pasa entonces a ser descriptivo y no un veredicto
var
Pdf: THotPDF;
Analysis: THPDFSignatureRevisionAnalysis;
begin
Pdf := THotPDF.Create(nil);
try
if Pdf.LoadFromFile('contract-countersigned.pdf') > 0 then
begin
if Pdf.AnalyzeLoadedSignatureRevisions(0, Analysis) then
begin
if Analysis.PolicyCompliant then
Writeln('Post-signature changes stay inside the signing policy')
else
Writeln('Policy violation: ', string(Analysis.Issue));
end
else
Writeln('Analysis could not run: ', string(Analysis.Issue));
end;
finally
Pdf.Free;
end;
end;
Para la triaje normalmente quieres el desglose por revisión en lugar del resumen, porque indica en qué momento de la historia del documento se torcieron las cosas. Cada entrada en Analysis.Revisions lleva su índice en la cadena, el desplazamiento de referencias cruzadas donde se escribió, su propio nivel de modificación y los números de objeto implicados
const
LevelNames: array[THPDFRevisionModificationLevel] of string =
('none', 'long-term validation', 'form fill and sign',
'annotations', 'other');
var
I: Integer;
begin
Writeln(Format('%d revisions in chain, signature sits at index %d',
[Analysis.TotalRevisionCount, Analysis.SignedRevisionIndex]));
for I := 0 to High(Analysis.Revisions) do
Writeln(Format(' rev %d at offset %d: %s (%d changed, %d freed) %s',
[Analysis.Revisions[I].RevisionIndex,
Analysis.Revisions[I].XRefOffset,
LevelNames[Analysis.Revisions[I].ModificationLevel],
Length(Analysis.Revisions[I].ChangedObjectNumbers),
Length(Analysis.Revisions[I].FreedObjectNumbers),
string(Analysis.Revisions[I].Issue)]));
end;
FieldMDP se evalúa por separado, y eso es deliberado
Un documento puede satisfacer DocMDP y aun así no ser legítimo, por lo que FieldMDPCompliant es un booleano distinto en lugar de estar plegado en la comparación de niveles. La norma ISO 32000-1 §12.8.2.4 define la transformación FieldMDP, y el §12.7.5.5 la entrada relacionada /SigFieldLock, para congelar campos de formulario con nombre en el momento de la firma incluso cuando el documento en su conjunto sigue permitiendo rellenar formularios. Rellenar un campo es una acción de nivel 2; rellenar un campo que el firmante bloqueó es una infracción independientemente del nivel. HotPDF lee el alcance en THPDFFieldLockAction como flaAll, flaInclude o flaExclude, con flaNone para resultados que no llevan ninguna política de bloqueo, y los nombres en Permissions.FieldNames: flaAll bloquea todo, flaInclude bloquea los nombres listados, flaExclude bloquea todo excepto ellos. Un detalle importa al leer los resultados: solo los campos ya presentes en la instantánea firmada se reportan en ChangedFieldNames, porque un campo creado enteramente después de la firma no tiene ningún estado firmado que contradecir y lo captura en su lugar la ruta de DocMDP
var
Source: TFileStream;
Analysis: THPDFSignatureRevisionAnalysis;
I: Integer;
begin
Source := TFileStream.Create('contract.pdf', fmOpenRead or fmShareDenyWrite);
try
if Pdf.AnalyzeLoadedSignatureRevisions(0, Source, Analysis) then
if Analysis.Permissions.HasFieldMDP and (not Analysis.FieldMDPCompliant) then
for I := 0 to High(Analysis.ChangedFieldNames) do
Writeln('modified after locking: ',
string(Analysis.ChangedFieldNames[I]));
finally
Source.Free; // stream position was restored before the call returned
end;
end;
Lo que este análisis no te dirá
No verifica una firma. AnalyzeLoadedSignatureRevisions razona sobre estructura y permisos; si el rango de bytes firmado sigue produciendo el hash del blob CMS, y si el certificado del firmante encadena hasta algo en lo que confías, lo responden VerifyLoadedSignature y VerifyLoadedSignatureWithTrust. Un archivo puede ser perfectamente conforme con la política y criptográficamente inútil, así que las dos comprobaciones deben ir juntas en cualquier puerta de aceptación real. Tampoco lee la intención dentro de los flujos de contenido: una página cuyo flujo de contenido se sustituyó por completo se detecta como un cambio fuera de la lista blanca, pero el análisis no te dirá que la sustitución cambió una cifra de pago. Un veredicto rmlOther significa que un humano debería mirarlo, no que haya habido fraude, y un veredicto conforme significa que el cambio encaja en una categoría permitida, no que el cambio fuera deseado. Cuando solo necesitas lo que declaró el firmante, sin el recorrido de revisiones, GetLoadedSignaturePermissions devuelve los diccionarios de política por sí solos
Todo lo descrito aquí se ejecuta de forma nativa en Delphi y C++Builder sin ningún servicio de firma externo en el proceso, lo que hace práctico ejecutarlo en cada documento entrante en lugar de solo en los que alguien ya sospechaba. La API completa de firmas y revisiones, incluidos los métodos de permisos y verificación con los que se combina, forma parte de HotPDF Component para Delphi y C++Builder