Artículo técnico

Firmas digitales PDF y PAdES en Delphi con HotPDF

Una firma PDF es sobre todo contabilidad de bytes, y la contabilidad de bytes es donde las cosas se tuercen. La criptografía corre sobre código auditado durante dos décadas, y esa parte casi nunca falla. Lo que falla en producción es más humilde: un marcador de posición reservado demasiado pequeño para la firma real, un hash calculado sobre el tramo equivocado del archivo, o un "guardar" tras firmar que reescribió en silencio bytes que la firma ya había congelado. Disponga los bytes correctamente y la marca verde se ocupa de sí misma

HotPDF cubre la firma para Delphi y C++Builder en tres niveles, y se elige entre ellos respondiendo a una sola pregunta: ¿dónde vive la clave privada? Un archivo PFX en disco necesita una única llamada a función. Una clave encerrada en un HSM o en un servicio de firma remoto necesita la secuencia reservar-hashear-insertar, porque ninguna biblioteca puede meterse en un token y sacar la clave. Una firma que tenga que satisfacer la normativa europea necesita además las estructuras PAdES baseline. Las secciones siguientes siguen esa progresión

Diagrama de decisión que elige entre la firma en una llamada desde PFX de HotPDF, la ruta reservar-hashear-insertar cuando la clave está en un HSM o servicio remoto, y las estructuras PAdES baseline para la firma regulada europea
Elija el nivel de firma preguntando dónde vive la clave privada; un archivo PFX legible reduce la firma a una sola llamada, mientras que las claves guardadas en un token obligan al desvío a nivel de bytes y la normativa europea añade la capa PAdES

Cómo /ByteRange fija los bytes firmados

Una firma tiene que vivir dentro del archivo que firma, y no puede firmarse a sí misma. PDF sortea la paradoja dejando un hueco. Antes de firmar, el escritor reserva una entrada /Contents de tamaño fijo llena de ceros y registra un array /ByteRange con los dos tramos a cada lado: todo lo anterior al hueco, todo lo posterior. El firmante calcula el hash de esos dos tramos y escribe el blob CMS resultante en el hueco como hexadecimal. La trampa está en la palabra fijo. Uno se compromete con el tamaño de ese hueco antes de saber cuánto ocupará la firma terminada, así que la reserva tiene que ser una sobreestimación segura. Ocho kilobytes albergan con holgura una firma CMS separada con una cadena de certificados corta

HotPDF separa los dos casos en dos llamadas, y confundirlas es un error temprano habitual. AddSignatureField deja un campo visible vacío para que una persona lo firme más tarde en un visor. AddSignedSignatureField crea el campo y reserva el hueco de /Contents, que es el que se quiere siempre que sea el código, y no un humano, quien complete la firma. Entregue a un firmante externo un campo vacío y no tendrá nada que rellenar

La ruta de una sola llamada: firmar desde un PFX

Cuando el certificado y su clave privada están en un archivo PFX/PKCS#12 que su proceso puede leer, todo el pipeline se reduce a una función de clase:

if THotPDF.SignPDFWithPFX('invoice-unsigned.pdf', 'invoice-signed.pdf',
    'company-cert.pfx', 'pfx-password') then
  Writeln('Signed: invoice-signed.pdf')
else
  raise Exception.Create('PFX signing failed');

Cuando esto falla, el PDF rara vez es el problema. Lo es el PFX. HotPDF lee contenedores protegidos con PBES2, es decir, derivación de clave PBKDF2 sobre AES-256-CBC. Un PFX exportado por un asistente de certificados de Windows antiguo, o por OpenSSL anterior a 3.0, suele estar envuelto en RC2 o 3DES heredados, y sencillamente no se analizará. La solución es volver a exportar el contenedor una vez con protección moderna; el OpenSSL actual lo hace por defecto, y no es un cambio de código. Así que cuando la firma muere al instante con un certificado que "funciona en todas partes", mire cómo se creó el PFX antes de sospechar de su propio código

La ruta reservar-hashear-insertar para HSM y tokens

La ruta de una sola llamada da por hecho que su proceso puede leer la clave como un archivo. Cada vez más no puede. La clave está en un HSM, en un token USB o detrás de la API de un servicio de firma, y una biblioteca no tiene forma de alcanzarla directamente. HotPDF lo resuelve dividiendo la firma en pasos a nivel de bytes: escribir un documento marcador, pedir a la biblioteca los rangos del hash, pasar la entrada del hash a lo que custodie la clave y después empalmar el CMS devuelto en el hueco

HotPDF: pipeline reservar-hashear-insertar en cuatro pasos sobre placeholder.pdf que muestra el hueco /Contents reservado entre los dos tramos de ByteRange y un HSM intercambiando el resumen por CMS en hexadecimal
HotPDF reserva el hueco e informa de ambos tramos de ByteRange, su custodio de claves los firma fuera, y el CMS devuelto se empalma byte a byte sin tocar ningún byte congelado
var
  Doc: THotPDF;
  Fs: TFileStream;
  PdfBytes, HashInput, SigHex: AnsiString;
  R1Start, R1Len, R2Start, R2Len, CStart, CLen: Integer;
begin
  // 1. Escribir el documento con un hueco /Contents reservado
  Doc := THotPDF.Create(nil);
  try
    Doc.FileName := 'placeholder.pdf';
    Doc.BeginDoc;
    Doc.CurrentPage.AddSignedSignatureField('Sig1',
      Rect(50, 100, 350, 150), 8192, 'adbe.pkcs7.detached',
      'Contract approval', 'Boston, MA', 'legal@example.com');
    Doc.EndDoc;
  finally
    Doc.Free;
  end;

  // 2. Cargar los bytes guardados; los desplazamientos devueltos empiezan en 0
  Fs := TFileStream.Create('placeholder.pdf', fmOpenRead);
  try
    SetLength(PdfBytes, Fs.Size);
    Fs.ReadBuffer(PdfBytes[1], Fs.Size);
  finally
    Fs.Free;
  end;
  THotPDF.PreparePDFForSigning(PdfBytes, R1Start, R1Len, R2Start, R2Len,
    CStart, CLen);

  // 3. Hashear ambos tramos y firmar externamente (HSM, token, servicio)
  HashInput := Copy(PdfBytes, R1Start + 1, R1Len) +
               Copy(PdfBytes, R2Start + 1, R2Len);
  SigHex := SignWithHsm(HashInput);  // su integración: devuelve el CMS en hexadecimal

  // 4. Empalmar la firma en el hueco reservado
  THotPDF.InsertSignatureHex(PdfBytes, SigHex);
  Fs := TFileStream.Create('signed.pdf', fmCreate);
  try
    Fs.WriteBuffer(PdfBytes[1], Length(PdfBytes));
  finally
    Fs.Free;
  end;
end;

Dos detalles de esta secuencia causan la mayoría de los fallos intermitentes. El primero es que PreparePDFForSigning trabaja sobre los bytes de un archivo terminado. El marcador tiene que escribirse y guardarse por completo antes de que los desplazamientos signifiquen algo; calcúlelos contra un stream que todavía se está ensamblando y no coincidirán con los bytes que finalmente hashee. El segundo es, de nuevo, el tamaño de la reserva. Los 8192 bytes que pidió tienen que albergar el CMS final, y una firma que arrastre certificados intermedios, o una que un servicio decore con atributos firmados, puede sobrepasarlos. InsertSignatureHex no agrandará el hueco para hacer sitio. La señal delatora es un pipeline que firma bien con un certificado y falla con el siguiente; el remedio es regenerar el marcador con una reserva medida a partir de una firma real producida por el firmante de verdad, no adivinada

Los baselines PAdES, y los sellos de tiempo que mantienen viva una firma

Si firma bajo las reglas europeas, la norma en juego es ETSI EN 319 142-1, que apila cuatro niveles PAdES baseline. B-B es la firma simple. B-T añade un sello de tiempo confiable que demuestra cuándo se hizo. B-LT incrusta el material de validación, los certificados y los datos de revocación, dentro del documento para que aún pueda comprobarse años después. B-LTA superpone sellos de tiempo de documento periódicos, de modo que la evidencia sobreviva a los algoritmos con los que se construyó. HotPDF emite las estructuras del lado del documento para cada nivel:

HotPDF: niveles PAdES baseline apilados desde B-B pasando por B-T y B-LT hasta B-LTA, con una línea temporal de renovación que muestra sellos de tiempo de documento periódicos manteniendo una firma verificable décadas después
Cada nivel apila nueva protección sobre el anterior; B-LTA sigue reaplicando sellos de tiempo de documento para que la evidencia sobreviva a los algoritmos con los que se construyó al principio
// Campo de firma PAdES baseline (ETSI EN 319 142-1)
Pdf.CurrentPage.AddPAdESSignatureField(
  'ApprovalSig', Rect(50, 100, 350, 150), 'B-B',
  'Contract approval', 'Boston, MA', 'legal@example.com');

// Sello de tiempo de documento: reserva mayor para el token de la TSA y su cadena
Pdf.CurrentPage.AddDocumentTimestampSignature('ArchiveTS', 16384);

La reserva de 16384 bytes del sello de tiempo es deliberada. Una autoridad de sellado de tiempo devuelve un token que arrastra su propia cadena de certificados, así que habitualmente necesita más espacio que los 8 KB con los que una firma simple se conforma. Esos sellos de tiempo de documento son también la maquinaria detrás de B-LTA: volver a sellar una firma archivada cada pocos años, con algoritmos todavía vigentes, es lo que mantiene verificable en 2040 un documento firmado en 2026

Una palabra sobre las cadenas de motivo, ubicación y contacto que aceptan ambas llamadas de campo: son metadatos de conveniencia y nada más. HotPDF los almacena como entradas de diccionario simples y los pinta en la apariencia visible de la firma, pero ningún validador los contrasta con nada. Rellénelos de forma coherente a partir de los datos de su flujo de trabajo, ya que los auditores sí los leen, y después nunca los confunda con evidencia. La afirmación criptográfica real vive por completo en el CMS y su cadena de certificados, y un verificador ignora el texto visible por completo

Después de firmar, el archivo solo puede crecer

En el momento en que existe una firma, los bytes dentro de sus rangos quedan congelados. La única forma legítima de cambiar el archivo después es una actualización incremental según ISO 32000-1 §7.5.6, que añade los objetos nuevos y modificados tras los bytes originales y encadena una nueva sección de referencias cruzadas hacia ellos. Hecho así, la firma sigue siendo válida para su revisión y el visor informa del estado honesto: la revisión firmada está intacta, el documento se amplió después. Vuelva a serializar el archivo completo en su lugar y reescribirá los tramos firmados, lo que destruye la firma aunque nada visible haya cambiado. Ese mismo mecanismo de revisiones es también la forma en que un documento lleva varias firmas: cada nueva firma aterriza en su propia actualización incremental, y sus rangos cubren todo lo anterior, incluidas las firmas previas. La mecánica de solo anexar, y cuándo es seguro compactarla, se trata en el artículo sobre flujos de objetos y actualizaciones incrementales

Conviene tener presentes dos límites mientras diseña. El modo de salida PDF/A de HotPDF rechaza de plano los campos de firma, así que la conformidad de archivado y una firma incrustada tienen que entregarse como archivos separados. Y firmar no dice nada sobre el secreto: demuestra quién produjo un documento y que no ha cambiado desde entonces, pero cualquiera puede seguir leyéndolo. Ocultar el contenido es un trabajo aparte, a cargo del cifrado AES-256 y la política de permisos

Construya lo que construya, pruébelo con algo distinto del código que escribió el archivo. Abra la salida en el panel de firmas de Acrobat y confirme tres cosas: la firma es válida, la identidad encadena hasta la raíz que esperaba y el panel no informa de cambios desde la firma. Después cambie un solo byte dentro del rango firmado de una copia desechable y confirme que el panel declara ahora el documento como alterado. Un pipeline de firma al que nunca ha visto rechazar un archivo manipulado es uno cuya verificación no se ha probado de verdad

Los tres niveles de firma se incluyen con el HotPDF Delphi Component para Delphi y C++Builder; la página del producto enlaza la referencia completa de la API de firma