Artículo técnico

Clasificar qué cambió en un PDF después de firmarlo

Una firma sobre un PDF no prohíbe los cambios posteriores. Fija un rango de bytes, y una actualización incremental añade bytes nuevos después, así que la firma sigue siendo matemáticamente válida mientras el documento adquiere contenido nuevo. Si ese contenido es aceptable es una pregunta de política, y DocMDP es donde el autor declara la política: ningún cambio, solo llenado de formularios y firma, o eso más anotaciones. Aplicarla significa clasificar qué cambió realmente, que es lo que hace AnalyzeModifications. Apúntenla a una revisión anterior y después lean GetModificationLevel para el veredicto general y los accesores por hallazgo para el nivel, el número de objeto y la descripción de cada diferencia

Con eso en su sitio, aplicar DocMDP se reduce a una comparación: si el nivel calculado está en el nivel que la política permite o por debajo

Diagrama de la escalera de niveles de modificación de PDFlibPas desde mlNone hasta mlUnclassified que muestra la aplicación de la política DocMDP como una comparación en Delphi
La escalera TPLModificationLevel va de mlNone a mlUnclassified y aplicar DocMDP se reduce a comparar el nivel calculado contra la política

Por qué se espera que un PDF firmado cambie

Tres casos legítimos, y cubren la mayor parte de lo que verán. Un segundo firmante añade su firma. Un destinatario llena campos de formulario que el autor dejó abiertos. Y se añade material de validación a largo plazo: respuestas OCSP y CRL escritas en el almacén de seguridad del documento para que la firma siga siendo verificable cuando los respondedores ya no existan. Ese último caso no solo está permitido, es lo que un archivo bien gestionado hace a propósito con los documentos firmados

Así que "el archivo creció después de firmar" no aporta información. La pregunta es siempre qué se añadió, y la respuesta tiene que salir de comparar estados del documento y no de observar bytes. La mecánica del anexado en sí se trata en el artículo de actualización incremental

Clasificar por la forma del objeto, no por la ruta que lo produjo

El clasificador mira lo que un objeto es después del cambio, no qué llamada de la biblioteca lo creó. Es deliberado, porque el análisis se ejecuta sobre archivos producidos por otro software, donde no hay ninguna ruta de llamadas que inspeccionar

Se reconocen cuatro formas. Los diccionarios de información del almacén de seguridad del documento y de validación, los objetos de flujo de referencias cruzadas, la entrada metadata del catálogo y los diccionarios de firma que llevan un rango de bytes son material de archivo a largo plazo. Un objeto que lleva tanto un tipo de campo como un valor de campo es llenado de formulario. Un objeto cuyo tipo es anotación, o cuyo subtipo es uno de los listados en la Tabla 168 de ISO 32000-2, es un cambio de anotación. Todo lo demás es sin clasificar

Árbol de decisión que PDFlibPas aplica a cada objeto PDF cambiado, ordenando las actualizaciones en niveles de archivo, llenado de formulario, anotación o sin clasificar
Cada objeto cambiado se clasifica por lo que es: almacén de seguridad, flujo xref, campo o anotación, nunca por la llamada que lo produjo

Las eliminaciones se tratan con más rigor que las adiciones. Un objeto eliminado entra en la lista blanca solo cuando el objeto del lado antiguo era él mismo material de archivo, lo que cubre el caso normal de un almacén de seguridad reemplazado por uno más nuevo. Toda otra eliminación es sin clasificar, porque borrar contenido de un documento firmado no es algo que un nivel de permiso autorice. Las diferencias a nivel de documento son más estrictas todavía: un cambio en el número de páginas va directo a sin clasificar sin examinar objetos individuales, ya que ningún nivel DocMDP permite añadir ni quitar páginas

La lista blanca se equivoca del lado de rechazar

Esta es la regla de diseño que gobierna cada decisión al límite. Un cambio clasificado erróneamente como permitido es una firma que valida sobre contenido que el autor nunca autorizó. Un cambio clasificado erróneamente como sin clasificar es un documento que se marca y revisa una persona. Esos dos errores no son simétricos, así que la lista blanca se mantiene estrecha y las formas no reconocidas caen en sin clasificar en lugar de adivinarse

Eso tiene una consecuencia práctica que conviene anticipar: los archivos de productores inusuales a veces reportarán cambios sin clasificar que, al examinarlos, son benignos. La respuesta correcta es mirar el detalle del hallazgo y el número de objeto en lugar de ensanchar la lista blanca, porque una lista blanca que crece para silenciar reportes individuales deja de ser un control de seguridad

uses
  PDFlibrary, PDFlibCompare;

var
  Pdf: TPDFlib;
  I, Level: Integer;
begin
  Pdf := TPDFlib.Create(nil);
  try
    Pdf.LoadFromFile('contract-countersigned.pdf', '');
    if Pdf.AnalyzeModifications('contract-as-signed.pdf', '') < 0 then
      raise Exception.Create('the earlier revision could not be loaded');

    // TPLModificationLevel ordenado mlNone, mlLTAUpdates, mlFormFilling,
    // mlAnnotations, mlUnclassified; el getter devuelve su ordinal
    Level := Pdf.GetModificationLevel;
    // Aplicar DocMDP es ahora una comparación contra la política
    if Level > Ord(mlFormFilling) then
      for I := 0 to Pdf.GetModificationFindingCount - 1 do
        Report.Add(Format('object %d, level %d: %s',
          [Pdf.GetModificationFindingObjNum(I),
           Pdf.GetModificationFindingLevel(I),
           Pdf.GetModificationFindingDetail(I)]));
  finally
    Pdf.Free;
  end;
end;

El nivel general es el máximo de todos los hallazgos, que es la única agregación defendible: un documento con noventa y nueve adiciones de archivo y un cambio sin clasificar es un cambio sin clasificar

Por debajo: huellas, no hashes criptográficos

El motor de comparación que expone CompareWith, y sobre el que se construye el análisis de modificaciones, identifica objetos por una huella de su cuerpo normalizado usando un hash de 64 bits no criptográfico en lugar de SHA-256. Es una decisión meditada. Lo que necesita una comparación estructural es determinismo: el mismo cuerpo de objeto debe producir siempre la misma huella dentro de una ejecución. No necesita resistencia a colisiones, porque un atacante que controla ambos lados de la comparación ya ganó por otros medios, y pagar un hash criptográfico completo por cada objeto en un documento de un millón de objetos es un costo real sin beneficio

Dos reglas de normalización importan más que la elección del hash. Las referencias indirectas se pliegan a un token de marcador de posición en lugar de expandirse al contenido referenciado: expandir copiaría el cuerpo de un objeto compartido en cada referenciador, así que una pequeña edición en un descriptor de fuente compartido invalidaría la huella de cada objeto que lo alcanza, y el reporte sería ilegible. Y los números de objeto en sí se excluyen de la huella, porque una reescritura puede renumerar objetos sin cambiar nada semántico

El emparejamiento corre luego en dos pasadas, alineando primero por huella y emparejando el resto por número de objeto para identificar cambios y no una adición más una eliminación. Las comprobaciones baratas van primero en todo: una diferencia en el número de páginas se reporta antes de que empiece cualquier recorrido de objetos

Diff de revisiones PDF en dos pasadas en PDFlibPas: primero la comprobación del número de páginas, huellas de 64 bits, alineación por huella y luego emparejamiento por número de objeto
El motor de comparación toma huellas de los cuerpos normalizados, reporta primero las diferencias en el número de páginas y luego empareja por huella y número de objeto

Una trampa: la autocomparación no garantiza idéntico

La primera prueba natural para un motor de diff es comparar un archivo consigo mismo y afirmar que el resultado es idéntico. Esa afirmación no se cumple aquí, y la razón es instructiva. La ruta pública de carga y la ruta de carga de documento de más bajo nivel no configuran la decodificación de forma idéntica, así que el mismo archivo cargado por las dos vías puede producir huellas que difieren para algunos objetos. El motor no está mal; las dos cargas genuinamente produjeron estados en memoria distintos

En lugar de forzar la unión de las dos rutas, la semántica de comparación se enuncia de forma estrecha: el análisis compara el estado actual del documento contra una revisión anterior, y reporta idéntico solo cuando los dos conjuntos de huellas coinciden exactamente. Esa es la pregunta que los usuarios hacen de verdad, y no requiere que los dos cargadores sean intercambiables. Al diseñar una función de comparación, definir qué significa "lo mismo" es más trabajo que calcularlo

Dónde usarlo

Dos lugares. En un reporte de validación, junto a la comprobación de firmas, para que quien revisa vea no solo si la firma está criptográficamente intacta sino qué pasó con el documento después; el lado de las firmas se trata en firma y validación PAdES. Y en una compuerta de admisión, donde un documento que llega de afuera se coteja con la copia que ustedes enviaron, de modo que un contrato devuelto con una anotación añadida se trate distinto de uno con una página editada

Una advertencia sobre el alcance. Este análisis les dice qué cambió entre dos revisiones de la misma línea de documentos. No les dice si el contenido visible es engañoso, si el flujo de apariencia de un campo de formulario coincide con su valor, o si el texto oculto bajo una superposición sigue presente en el flujo de contenido. Eso necesita tratamiento aparte, y el lado de eliminación de contenido se trata en el artículo de redacción verdadera. Los puntos de entrada de análisis y comparación están documentados en la página del producto losLab PDF Developer Library