Antes de que un visor muestre la marca verde en un PDF firmado, hace tres cosas mecánicas: lee el arreglo /ByteRange del diccionario de firma, calcula el hash exactamente sobre los dos tramos de bytes que ese arreglo describe y verifica la firma CMS almacenada, como hexadecimal, en la entrada /Contents que queda entre esos tramos. Casi toda falla de firma en producción se remonta a que uno de esos tres pasos quedó dispuesto de forma incorrecta: un marcador de posición demasiado pequeño para el blob final de firma, un hash calculado sobre los 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 da a las aplicaciones Delphi y C++Builder tres niveles de soporte de firma: firma PFX de una llamada, un flujo con firmante externo para HSM y servicios de firma, y campos con perfil PAdES y marcas de tiempo de documento. Se presentan aquí en ese orden porque cada nivel existe para manejar un modo de falla del anterior.
El contrato ByteRange en ISO 32000-1 §12.8
Una firma PDF debe vivir dentro del archivo que firma, lo que crea un problema circular: el valor de la firma no puede cubrirse a sí mismo. El formato lo resuelve con un hueco. El escritor reserva una entrada /Contents de tamaño fijo llena de ceros, y /ByteRange registra dos tramos: todo lo anterior al hueco y todo lo posterior. El firmante calcula el hash de esos dos tramos, y la estructura CMS resultante se escribe en el hueco como hexadecimal. La consecuencia que tropieza a los ingenieros: el tamaño de reserva queda congelado antes de firmar, así que la firma final, certificados incluidos, debe caber en un hueco elegido antes. Alrededor de 8 KB alcanza para una firma CMS separada con una cadena corta de certificados.
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 la llamada equivocada es un error clásico de primera semana: un campo vacío no le da a un firmante externo nada que llenar.
Una llamada cuando la clave está en un archivo PFX
Cuando el certificado de firma y la clave privada viven en un archivo 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');
La falla que domina el tráfico de soporte aquí no tiene que ver con el PDF: es el contenedor PFX en sí. HotPDF lee archivos 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 de forma predeterminada protección heredada RC2/3DES y no se analizarán. El remedio es una reexportación única del contenedor con parámetros modernos, lo que OpenSSL actual hace por defecto, no un cambio de código. Revisen esto primero cuando la firma falle de inmediato con un certificado que "funciona en todas partes".
Firma externa: reservar, calcular hash, insertar
La ruta de una llamada supone que la clave privada es un archivo que su proceso puede leer. Cada vez más, las claves de firma de 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 marcador, calcular los rangos de hash, entregar la entrada de hash a quien tenga la clave y empalmar el CMS devuelto de vuelta.
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 provocan fallas intermitentes en producción cuando se pasan por alto. Primero, PreparePDFForSigning opera sobre los bytes de un archivo completamente guardado: el documento marcador debe escribirse hasta el final antes de que los rangos signifiquen algo; calcular rangos contra un stream en curso produce desplazamientos que ya no coinciden con la serialización final. Segundo, la reserva de 8192 bytes debe contener el CMS final. Una firma que embebe certificados intermedios, o una devuelta por un servicio que agrega atributos firmados, puede excederla, y InsertSignatureHex no puede agrandar el hueco. El síntoma es un trabajo que tiene éxito con un certificado y falla con otro; la corrección es regenerar el marcador con una reserva mayor, dimensionada a partir de una firma real producida por el firmante real.
Baselines PAdES y marcas de tiempo de documento
La regulación europea de firmas se basa en ETSI EN 319 142-1, que define cuatro niveles baseline PAdES: B-B es la firma básica; B-T agrega una marca de tiempo confiable que prueba cuándo se realizó; B-LT embebe el material de validación, certificados y datos de revocación, dentro del documento; B-LTA agrega marcas de tiempo periódicas del 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);
Observen la reserva de 16384 bytes en la marca de tiempo: el token de una autoridad de sellado de tiempo trae su propia cadena de certificados, por lo que habitualmente supera los 8 KB suficientes para una firma simple. Las marcas de tiempo de documento también son el mecanismo detrás del mantenimiento B-LTA: volver a sellar un archivo firmado cada ciertos años con algoritmos actuales es lo que mantiene verificable en 2040 una firma de 2026.
Las cadenas de razón, 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 complétenlas de manera consistente desde los datos del flujo. Pero nunca traten 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 archivos 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 el archivo después: los objetos nuevos y modificados se agregan después de los bytes originales, con una nueva sección de referencias cruzadas que encadena hacia atrás. La firma sigue siendo válida para su revisión, y los visores informan el estado honesto: "revisión firmada intacta, documento modificado después". Una reserialización completa, en cambio, reescribe los tramos firmados y destruye la firma por completo, aunque no cambie ni un elemento visible. La mecánica de guardados append-only y cuándo compactarlos se cubre en el artículo sobre object streams y actualizaciones incrementales.
Una restricción a nivel de biblioteca completa el panorama de planificación: el modo de salida PDF/A de HotPDF rechaza campos de firma, así que la conformidad de archivo y las firmas embebidas deben dividirse en entregables separados en lugar de combinarse en un solo archivo. 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 para una tubería de firma deben ser independientes del código que produjo el archivo. Abran la salida en el panel de firmas de Acrobat y confirmen tres estados: la firma es válida, la identidad encadena hasta la raíz esperada y el panel informa que no hay cambios después de la firma. Luego corrompan un byte dentro del rango firmado de una copia y confirmen que el mismo panel reporta el documento como alterado: una tubería que nunca se ha visto fallar la validación es una tubería cuya validación no se ha probado realmente.
Preguntas que aparecen en revisiones de código de firma
¿Qué tamaño debe tener la reserva /Contents?
8192 bytes para un CMS separado con una cadena corta; 16384 cuando intervienen marcas de tiempo o intermedios embebidos. Midan el CMS que produce su firmante real y agreguen margen: la reserva no puede crecer después.
¿Un documento puede 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.
¿Firmar protege el contenido del documento?
No. Una firma proporciona integridad y evidencia de origen; cualquiera puede seguir leyendo el archivo. La confidencialidad requiere cifrado, configurado de forma independiente.
Los tres niveles de firma se incluyen con HotPDF Component para Delphi y C++Builder; la página del producto enlaza la referencia completa de la API de firma.