Artículo técnico

DocMDP y FieldMDP: auditar revisiones de PDF en Delphi

Un PDF firmado que cambió después de la firma no está automáticamente roto. ISO 32000-1 permite actualizaciones incrementales encima de 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 contra DocMDP y FieldMDP. El escenario es familiar para cualquiera que distribuya software de contratos: tu cliente firma un contrato de compra, lo envía, y lo recibe de vuelta con una página anexa adjunta. El lector muestra una barra amarilla diciendo que la firma está intacta pero el documento ha sido modificado desde que se firmó, y nadie en la sala puede decir si eso es un flujo de trabajo de contrafirma normal o alguien editando silenciosamente un contrato firmado

¿Qué cuenta como un cambio legal después de firmar?

Un cambio es legal cuando su categoría semántica cae dentro del permiso que declaró la firma certificadora. 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 anotar. HotPDF expone eso como 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 ordenamiento es el motor de toda la verificación. THPDFRevisionModificationLevel ejecuta 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 DocMDP se convierte en una sola prueba de enteros. Un matiz importa desde temprano: en dmpNoChanges el análisis todavía acepta rmlLongTermValidation. Agregar material de validación DSS y VRI o una marca de tiempo de documento a un archivo certificado es mantenimiento de la firma, no modificación del documento, y tratarlo como una violación rompería cada flujo de trabajo de archivo a largo plazo que existe

¿Cómo reconstruye HotPDF la cadena de revisiones?

De forma estructural, no heurística. Según ISO 32000-1 §7.5.6 una actualización incremental agrega una nueva sección de referencia cruzada cuyo /Prev apunta a la anterior, así que HotPDF lee startxref desde el final, analiza la sección ahí, sigue /Prev hacia atrás y repite, devolviendo las secciones de la más antigua a la más nueva. Dos límites de seguridad se encuentran en ese bucle y ambos vale 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 vez de girar sin fin, y una cadena más larga de mil revisiones se rechaza de inmediato. Ambos aparecen en Analysis.Issue con la función devolviendo False, y ninguno debería disimularse, porque un /Prev cíclico es un archivo malformado u hostil en vez de uno simplemente inusual

Se dan cuatro formas históricas en documentos reales y las cuatro se manejan: tablas xref tradicionales analizadas línea por línea, flujos de referencia cruzada descomprimidos y decodificados a través de sus campos /W y /Index, archivos de referencia híbrida cuyo trailer tradicional lleva una clave /XRefStm que se analiza y fusiona en la misma revisión (el caso del productor de Office, cubierto en el artículo sobre flujos de referencia cruzada híbridos), y objetos que viven dentro de un contenedor ObjStm, que importan porque una actualización moderna generalmente pone el diccionario cambiado en un flujo comprimido en vez de escribirlo directamente, como se describe en la pieza sobre flujos de objetos y actualizaciones incrementales. La firma ancla la división: /ByteRange[2] + /ByteRange[3] se convierte en SignedRevisionLength, y cada sección en o más allá de ese desplazamiento es posterior a la firma. Si el rango de bytes todavía hashea correctamente es una pregunta separada, respondida por VerifyLoadedSignature y cubierta en el artículo sobre verificar firmas digitales de PDF

Cómo se clasifica cada objeto cambiado

La clasificación se ejecuta por objeto, luego se propaga a través de las referencias. Para cada número de objeto que toca una sección posterior a la firma, HotPDF lee el cuerpo nuevo y el cuerpo tal como estaba en el snapshot firmado; 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 es ETSI.RFC3161, es rmlLongTermValidation, al igual que cualquier cosa accesible desde el árbol /DSS del catálogo; un diccionario /Type /Sig es rmlFormFillAndSign. Para 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 anexa agregada: agregar una página reorganiza el árbol de páginas de formas que ninguna lista blanca cubre, y ninguna cantidad de relleno de formulario legítimo se le parece

Luego los niveles se propagan, con cada contenedor heredando 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 funcionar los flujos de apariencia. Un campo de texto rellenado reescribe /V y apunta a un flujo /AP fresco, y ese flujo por sí solo es un blob anónimo de operadores de contenido sin tipo que reconocer; porque el campo que lo posee es rmlFormFillAndSign, el flujo hereda el mismo nivel en vez de caer en rmlOther. La misma propagación lleva contexto DSS a flujos de certificado y revocación que de otro modo serían inclasificables

¿Por qué un objeto ilegible cuenta como violación?

Porque la alternativa es un validador derrotado escribiendo algo que no entiende. Tres situaciones terminan en rmlOther sin apelación en HotPDF: un objeto cuyo cuerpo no pudo leerse 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, así que un operador puede ver qué número de objeto produjo el veredicto

Liberar es la más severa de las tres. Una revisión posterior a la firma que marca un objeto previamente definido como libre ha eliminado contenido de un documento firmado, y ningún nivel de permiso bajo §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 una razón diferente. Un validador que no puede analizar un objeto no tiene base para llamarlo inofensivo, y la respuesta honesta a eso no es el silencio. Reportar una construcción inusual pero benigna como una violación cuesta una revisión humana; el error opuesto envía un contrato firmado con una edición no detectada dentro

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 de TStream toma bytes suministrados por quien llama y restaura la posición del flujo antes de retornar. PolicyCompliant es el único booleano que la mayoría de quienes llaman quieren, combinando tres decisiones independientes: la validez estructural de los diccionarios de permisos, DocMDPCompliant, y FieldMDPCompliant. Mantén los componentes visibles en tu interfaz en vez de colapsarlos, y nota 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 violar y el ModificationLevel agregado es entonces descriptivo en vez de 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 triaje generalmente quieres el desglose por revisión en vez del resumen, porque indica cuándo en la historia del documento las cosas salieron mal. Cada entrada en Analysis.Revisions lleva su índice en la cadena, el desplazamiento de referencia cruzada en el que fue escrita, su propio nivel de modificación, y los números de objeto involucrados

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 juzga por separado, y eso es deliberado

Un documento puede satisfacer DocMDP y aun así ser ilegítimo, por lo que FieldMDPCompliant es un booleano distinto en vez de plegarse en la comparación de nivel. ISO 32000-1 §12.8.2.4 define la transformación FieldMDP, y §12.7.5.5 la entrada relacionada /SigFieldLock, para congelar campos de formulario nombrados en el momento de la firma incluso donde el documento en su conjunto todavía permite rellenar formularios. Rellenar un campo es una acción de nivel 2; rellenar un campo que el firmante bloqueó es una violación sin importar el nivel. HotPDF lee el alcance en THPDFFieldLockAction como flaAll, flaInclude o flaExclude, con flaNone para resultados que no llevan 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, y es que solo se reportan en ChangedFieldNames los campos ya presentes en el snapshot firmado, porque un campo creado enteramente después de la firma no tiene estado firmado que contradecir y en su lugar es capturado por la ruta 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 todavía hashea al valor del blob CMS, y si el certificado del firmante encadena a algo en lo que confías, son preguntas que responden VerifyLoadedSignature y VerifyLoadedSignatureWithTrust. Un archivo puede ser perfectamente conforme con la política y criptográficamente inútil, así que las dos verificaciones pertenecen lado a lado 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 fue reemplazado por completo se detecta como un cambio fuera de la lista blanca, pero el análisis no te dirá que el reemplazo cambió una cifra de pago. Un veredicto rmlOther significa que un humano debe mirar, no que ocurrió fraude, y un veredicto conforme significa que el cambio encaja en una categoría permitida, no que el cambio fue deseado. Cuando todo lo que necesitas es 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 que sea práctico ejecutarlo en cada documento entrante en vez de solo en los que alguien ya sospechaba. La API completa de firma y revisión, incluyendo los métodos de permiso y verificación con los que se combina, es parte de HotPDF Component para Delphi y C++Builder