Antes de que un visor muestre la marca verde en un PDF firmado, hace tres cosas mecánicas: lee el array /ByteRange del diccionario de firma, calcula el hash exactamente de los dos intervalos de bytes que describe ese array, y verifica la firma CMS almacenada, como hexadecimal, en la entrada /Contents situada entre esos intervalos. Casi todos los fallos de firma en producción se remontan a que uno de esos tres pasos quedó mal dispuesto: un marcador demasiado pequeño para el blob de firma final, un hash calculado sobre bytes equivocados o un guardado posterior a la firma que reescribió bytes ya cubiertos por los rangos. La criptografía casi nunca falla. La contabilidad de bytes sí
HotPDF ofrece a las aplicaciones Delphi y C++Builder tres niveles de soporte de firma: firma PFX en una llamada, un flujo con firmante externo para HSMs y servicios de firma, y campos de perfil PAdES con sellos de tiempo de documento. Se presentan aquí en ese orden, porque cada nivel existe para manejar un modo de fallo del anterior
El contrato ByteRange en ISO 32000-1 §12.8
Una firma PDF debe vivir dentro del fichero que firma, lo que crea una dependencia circular: el valor de firma no puede cubrirse a sí mismo. El formato lo resuelve con un hueco. El escritor reserva una entrada /Contents de tamaño fijo rellena con ceros, y /ByteRange registra dos intervalos: todo lo anterior al hueco y todo lo posterior. El firmante calcula el hash de esos dos intervalos y la estructura CMS resultante se escribe dentro del hueco como hexadecimal. La consecuencia que suele pillar a los ingenieros: el tamaño de la reserva queda congelado antes de firmar, así que la firma final, certificados incluidos, tiene que caber en un hueco elegido con anterioridad. Unos 8 KB alojan una firma CMS detached con una cadena de certificados corta
HotPDF expone la distinción directamente. AddSignatureField crea un campo visible vacío para que alguien lo firme después en un visor; AddSignedSignatureField crea el campo y además reserva el hueco /Contents para completarlo programáticamente. Elegir el método equivocado es un error clásico de la primera semana: un campo vacío no da al firmante externo nada que rellenar
Una llamada cuando la clave es un fichero PFX
Cuando el certificado de firma y la clave privada viven en un fichero PFX/PKCS#12, toda la tubería 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');
El fallo que domina el tráfico de soporte aquí no tiene nada que ver con el PDF: es el propio contenedor PFX. HotPDF lee ficheros PFX protegidos con PBES2, derivación de clave PBKDF2 con AES-256-CBC. Los contenedores exportados por asistentes antiguos de certificados de Windows, o por versiones de OpenSSL anteriores a 3.0, usan por defecto protección heredada RC2/3DES y no se analizan. El remedio es una reexportación única del contenedor con parámetros modernos, algo que OpenSSL actual hace por defecto, no un cambio de código. Compruebe esto primero cuando la firma falle inmediatamente con un certificado que "funciona en todas partes"
Firma externa: reservar, calcular hash, insertar
La ruta de una llamada presupone que la clave privada es un fichero que su proceso puede leer. Cada vez más, las claves de firma en producción no lo son: están en un HSM, un token USB o un servicio remoto de firma, y ninguna biblioteca puede llamarlas directamente. Para esa topología, HotPDF divide el flujo en pasos a nivel de bytes: escribir un documento con marcador, calcular los rangos de hash, entregar la entrada de hash a lo que posea la clave y empalmar de vuelta el CMS devuelto
var
Doc: THotPDF;
Fs: TFileStream;
PdfBytes, HashInput, SigHex: AnsiString;
R1Start, R1Len, R2Start, R2Len, CStart, CLen: Integer;
begin
// 1. Write the document with a reserved /Contents hole
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. Load the saved bytes; the returned offsets are 0-based
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. Hash both spans and sign externally (HSM, token, service)
HashInput := Copy(PdfBytes, R1Start + 1, R1Len) +
Copy(PdfBytes, R2Start + 1, R2Len);
SigHex := SignWithHsm(HashInput); // your integration: returns CMS as hex
// 4. Splice the signature into the reserved hole
THotPDF.InsertSignatureHex(PdfBytes, SigHex);
Fs := TFileStream.Create('signed.pdf', fmCreate);
try
Fs.WriteBuffer(PdfBytes[1], Length(PdfBytes));
finally
Fs.Free;
end;
end;
Dos restricciones de esta secuencia causan fallos intermitentes en producción cuando se olvidan. Primero, PreparePDFForSigning opera sobre los bytes de un fichero completamente guardado: el documento con marcador debe escribirse hasta el final antes de que los rangos signifiquen algo; calcular rangos contra un stream en curso produce offsets que ya no coinciden con la serialización final. Segundo, la reserva de 8192 bytes debe alojar el CMS final. Una firma que incrusta certificados intermedios, o una devuelta por un servicio que añade atributos firmados, puede excederla, y InsertSignatureHex no puede agrandar el hueco. El síntoma es un trabajo que funciona con un certificado y falla con otro; la solución es regenerar el marcador con una reserva mayor, dimensionada a partir de una firma real producida por el firmante real
Líneas base PAdES y sellos de tiempo de documento
La regulación europea de firma se basa en ETSI EN 319 142-1, que define cuatro niveles base PAdES: B-B es la firma básica; B-T añade un sello de tiempo de confianza que prueba cuándo se realizó; B-LT incrusta el material de validación, certificados y datos de revocación, dentro del documento; B-LTA añade sellos de tiempo periódicos de documento para que la evidencia sobreviva al envejecimiento de algoritmos. HotPDF crea las estructuras del lado del documento para este ciclo de vida
// PAdES baseline signature field (ETSI EN 319 142-1)
Pdf.CurrentPage.AddPAdESSignatureField(
'ApprovalSig', Rect(50, 100, 350, 150), 'B-B',
'Contract approval', 'Boston, MA', 'legal@example.com');
// Document timestamp: larger reservation for the TSA token and chain
Pdf.CurrentPage.AddDocumentTimestampSignature('ArchiveTS', 16384);
Observe la reserva de 16384 bytes en el sello de tiempo: el token de una autoridad de sellado de tiempo lleva su propia cadena de certificados, por lo que suele superar los 8 KB suficientes para una firma simple. Los sellos de tiempo de documento son también el mecanismo que sostiene el mantenimiento B-LTA: volver a sellar un archivo firmado cada pocos años con algoritmos actuales es lo que mantiene verificable en 2040 una firma de 2026
Las cadenas de motivo, ubicación y contacto aceptadas por ambas llamadas de campo merecen una nota de política: se almacenan como entradas simples de diccionario y se renderizan en la apariencia visible, pero nada las verifica. Documentan la intención para lectores humanos, "Contract approval", una ciudad, un buzón, y los auditores las leerán, así que rellénelas de forma coherente desde datos del flujo. Pero nunca trate el texto visible como evidencia: la declaración criptográfica vive por completo en el CMS y su cadena de certificados, y un validador ignora la apariencia por completo
Por qué los ficheros firmados solo deben crecer
Una vez que existe una firma, los bytes dentro de sus rangos quedan congelados para siempre. Las actualizaciones incrementales de ISO 32000-1 §7.5.6 son la única forma legítima de cambiar después el fichero: los objetos nuevos o modificados se anexan tras los bytes originales, con una nueva sección de referencias cruzadas encadenada hacia atrás. La firma sigue siendo válida para su revisión, y los visores informan del estado honesto: "revisión firmada intacta, documento modificado después". Una reserialización completa, en cambio, reescribe los intervalos firmados y destruye directamente la firma, aunque no haya cambiado ni un solo elemento visible. La mecánica de los guardados append-only y cuándo compactarlos se cubren en el artículo sobre object streams y actualizaciones incrementales
Una restricción a nivel de biblioteca completa la planificación: el modo de salida PDF/A de HotPDF rechaza campos de firma, así que la conformidad de archivo y las firmas incrustadas deben separarse en entregables distintos en vez de combinarse en un solo fichero. Y firmar es ortogonal a la confidencialidad: una firma prueba origen e integridad, pero no oculta nada, que es el terreno de el cifrado AES-256 y la política de permisos
Las pruebas de aceptación de una tubería de firma deberían ser independientes del código que produjo el fichero. Abra la salida en el panel de firmas de Acrobat y confirme tres estados: la firma es válida, la identidad encadena hasta la raíz esperada y el panel informa de que no hubo cambios después de firmar. Después corrompa un byte dentro del rango firmado de una copia y confirme que el mismo panel informa de que el documento fue alterado; una tubería que nunca se ha visto fallar la validación es una tubería cuya validación no se ha probado de verdad
Preguntas habituales en revisiones de código de firma
¿Qué tamaño debe tener la reserva /Contents?
8192 bytes para un CMS detached con una cadena corta; 16384 cuando intervienen sellos de tiempo o certificados intermedios incrustados. Mida el CMS que produce su firmante real y añada margen: la reserva no puede crecer después
¿Puede un documento llevar dos firmas?
Sí. Cada firma vive en su propia revisión incremental, y los rangos de la segunda firma cubren la primera, que es exactamente como se construyen los flujos de contrafirma
¿La firma protege el contenido del documento?
No. Una firma aporta evidencia de integridad y origen; cualquiera puede seguir leyendo el fichero. La confidencialidad requiere cifrado, configurado de forma independiente
Los tres niveles de firma se entregan con HotPDF Component para Delphi y C++Builder; la página del producto enlaza la referencia completa de la API de firmas