La pregunta que rompe los flujos débiles de firma casi nunca es criptográfica. Un auditor pregunta: "su informe preflight dice que este lote de facturas cumple PDF/A — ¿eso se verificó antes o después de aplicar la firma?" Cuando la validación y la firma se ejecutan como dos herramientas separadas con un paso de remediación entre ambas, existen al menos tres revisiones del archivo, y el informe describe solo una de ellas. PDFlibPas, la biblioteca PDF para desarrolladores de losLab para Delphi y C++Builder, ofrece preflight y firma PAdES detrás de una sola clase fachada, lo que permite construir un banco de trabajo donde esa pregunta tenga una respuesta comprobable.
Este artículo recorre el patrón completo del banco de trabajo: preflight sobre los bytes exactos que se firmarán, una firma aplicada mediante la API SignProcess, y una auditoría de lectura posterior que confirma que el ByteRange realmente cubre el archivo. Todas las llamadas mostradas aquí existen hoy en la biblioteca, y también todas las trampas.
Tres revisiones de un documento y cómo se abre la brecha
Un flujo de cumplimiento y luego firma toca el archivo al menos tres veces. El original llega desde el sistema anterior. Un paso de remediación lo carga, habilita un modo de cumplimiento y guarda una revisión corregida. Luego el paso de firma agrega una firma como actualización incremental. Cada guardado cambia bytes, por lo que un informe preflight solo es significativo si indica qué revisión describe. La forma más barata de anclar eso es registrar un hash SHA-256 del archivo junto a cada ejecución de preflight y cada firma.
Un comportamiento de la biblioteca hace que ese anclaje sea más estricto de lo que quizá esperan: las correcciones de cumplimiento solicitadas con SetPDFAMode o SetPDFUAMode se aplican durante el guardado, no en el momento de llamar esos métodos. Las reparaciones automáticas, como forzar las marcas de impresión en anotaciones o asignar un orden de tabulación PDF/UA, aterrizan solo en el archivo de salida. Ejecutar el verificador contra el documento que acaban de "arreglar" en memoria no prueba nada sobre los bytes que están a punto de firmar — siempre vuelvan a ejecutar preflight contra el archivo guardado.
Preflight desde disco y el cero que significa dos cosas
El punto de entrada plano de preflight es CheckFileCompliance(FileName, Password, ComplianceTest, Options), donde la prueba 1 selecciona PDF/A (ISO 19005) y la prueba 2 selecciona PDF/UA (ISO 14289). Abre el archivo mediante el lector de streaming de la biblioteca — sin necesidad de llamar antes a LoadFromFile — y devuelve un handle de lista de cadenas con un hallazgo por entrada:
var
PDF: TPDFlib;
ListID, I: Integer;
begin
PDF := TPDFlib.Create;
try
ListID := PDF.CheckFileCompliance('invoice-fixed.pdf', '', 1, 0); // 1 = PDF/A
if ListID = 0 then
begin
if PDF.LastErrorCode <> 0 then
raise Exception.Create('Preflight could not read the file')
else
Writeln('No PDF/A findings');
end
else
begin
for I := 0 to PDF.GetStringListCount(ListID) - 1 do
Writeln(PDF.GetStringListItem(ListID, I));
PDF.ReleaseStringList(ListID);
end;
finally
PDF.Free;
end;
end;
La trampa está en el valor de retorno. Cero significa "sin hallazgos", pero también significa "el archivo no pudo abrirse" — la implementación devuelve 0 cuando la lista de resultados termina vacía, incluso ante un fallo de lectura. Un banco de trabajo que trate 0 como luz verde aprobará un archivo bloqueado por otro proceso. Combinen la llamada con LastErrorCode, como arriba. Noten también que el verificador abre el archivo con un modo de uso compartido que niega escritura; si el paso de remediación todavía conserva un handle de escritura, preflight falla por una razón que no tiene nada que ver con cumplimiento.
Para revisión humana, CreatePreflightReport representa los mismos hallazgos como un informe legible, y ComparePreflightReports compara dos ejecuciones — una forma conveniente de probar que la remediación eliminó hallazgos sin introducir otros nuevos.
Firma de la revisión verificada con SignProcess
Una vez que la revisión guardada pasa preflight y su hash queda registrado, firmen exactamente ese archivo. La API SignProcess es un constructor: abre un handle de proceso, lo configura, lo confirma y luego lee el código de resultado.
ProcessID := PDF.NewSignProcessFromFile('invoice-fixed.pdf', '');
if ProcessID = 0 then
raise Exception.Create('Cannot open source for signing');
PDF.SetSignProcessField(ProcessID, 'ApprovalSig');
PDF.SetSignProcessPFXFromFile(ProcessID, 'company.pfx', PfxPassword);
PDF.SetSignProcessInfo(ProcessID, 'Invoice approval', 'Berlin', 'billing@example.com');
PDF.SetSignProcessCustomSubFilter(ProcessID, 'ETSI.CAdES.detached'); // PAdES baseline
PDF.SetSignProcessDigestAlgorithm(ProcessID, 2); // SHA-256
PDF.SetSignProcessReserveContentsBytes(ProcessID, 8192); // room for a later timestamp
PDF.EndSignProcessToFile(ProcessID, 'invoice-signed.pdf');
if PDF.GetSignProcessResult(ProcessID) <> 1 then
Writeln('Sign failed, code ', PDF.GetSignProcessResult(ProcessID));
PDF.ReleaseSignProcess(ProcessID);
Dos líneas de configuración merecen atención. SetSignProcessCustomSubFilter con ETSI.CAdES.detached selecciona una firma PAdES según el perfil de ETSI EN 319 142-1, en lugar de la familia heredada adbe.pkcs7.detached. Y SetSignProcessReserveContentsBytes rellena el marcador de posición /Contents: si alguna vez van a agregar una marca de tiempo de firma, el CMS ampliado debe caber en el espacio reservado ahora, porque el marcador no puede crecer después sin volver a firmar.
GetSignProcessResult devuelve resultados codificados: 1 es éxito, 4 significa contraseña PDF incorrecta, 7 contraseña de certificado incorrecta, 9 un PFX sin clave privada, 11 un fallo al aplicar la firma. Registren el código en lugar de un booleano — gran parte de los casos de soporte sobre firmas son confusiones de credenciales que solo esos valores permiten distinguir.
Lectura posterior: auditoría del archivo que acaban de producir
Un banco de trabajo nunca debería confiar en su propia ruta de escritura. La clase de auditoría TPDFlibSignDoc vuelve a abrir la salida firmada y expone directamente las entradas del diccionario de firma:
var
Doc: TPDFlibSignDoc;
Names: TStringList;
FS: TFileStream;
I: Integer;
SourceSize, RangeStart, GapStart, TailStart, TailLen: Int64;
begin
// Capture the size before Open: the audit object holds a share lock on the file
FS := TFileStream.Create('invoice-signed.pdf', fmOpenRead or fmShareDenyNone);
SourceSize := FS.Size;
FS.Free;
Doc := TPDFlibSignDoc.Create;
Names := TStringList.Create;
try
if not Doc.Open('invoice-signed.pdf', '', False) then Exit;
Doc.GetSignatureFieldNames(Names);
for I := 0 to Names.Count - 1 do
if Doc.GetSignatureValueObjNum(Names[I]) > 0 then // > 0 means the field is signed
begin
RangeStart := StrToInt64(string(Doc.GetSignatureValueByName(Names[I], 11)));
GapStart := StrToInt64(string(Doc.GetSignatureValueByName(Names[I], 12)));
TailStart := StrToInt64(string(Doc.GetSignatureValueByName(Names[I], 13)));
TailLen := StrToInt64(string(Doc.GetSignatureValueByName(Names[I], 14)));
if (RangeStart = 0) and (TailStart + TailLen = SourceSize) then
Writeln(Names[I], ': signature covers the file to EOF')
else
Writeln(Names[I], ': earlier revision, or unusual ByteRange layout');
end;
Doc.Close;
finally
Names.Free;
Doc.Free;
end;
end;
Los argumentos ValueKey se asignan a entradas del diccionario: 0 devuelve el CMS sin procesar de /Contents, 2 y 3 los nombres /Filter y /SubFilter, y 11 a 14 los cuatro números de ByteRange. Los valores de texto pasan por GetSignatureTextValueByName — la clave 0 es la hora de firma declarada, la clave 5 distingue una firma ordinaria Sig de un DocTimeStamp.
La captura del tamaño del archivo al principio no es decorativa. TPDFlibSignDoc.Open mantiene el archivo con un bloqueo de uso compartido restrictivo durante toda su vida útil, de modo que cualquier tarea que necesite los bytes crudos — calcular hash del rango firmado, recalcular el digest CMS — debe leer el archivo antes de Open. La propia demo SigningWorkbench de la biblioteca lee todo el archivo en memoria primero exactamente por esta razón.
Aritmética ByteRange que prueba la cobertura
Un archivo sano con una sola firma tiene un ByteRange de la forma [0 a b c]: la cobertura inicia en el offset 0, salta el marcador hexadecimal /Contents entre a y b, y luego continúa hasta el byte b+c. Cuando b+c es igual al tamaño del archivo, la firma cubre todo hasta el final. Cuando no lo es, alguien agregó una actualización incremental después de firmar. Eso es legítimo bajo ISO 32000-1 §12.8 — rellenos posteriores de formularios, una segunda firma, un diccionario DSS, todo llega de esa forma — pero es precisamente el dato que una pista de auditoría debe registrar desde el inicio, no descubrir durante una disputa.
Cuiden el ancho de entero al comprobarlo. La API plana GetSignProcessByteRange devuelve un Integer de 32 bits, mientras que los valores subyacentes son Int64; en archivos de más de 2 GB, el accesor plano trunca. Usen la capa de clases TPDFlibSigner.GetByteRange, que devuelve Int64, o analicen los valores desde GetSignatureValueByName como hace el código de auditoría anterior.
Lo que sigue siendo responsabilidad de ustedes
Sean claros sobre los límites. La API plana TPDFlib no tiene ningún wrapper de verificación de firma; la verificación criptográfica vive en la capa de clases como TPDFlibSignatureVerifier, cuyo VerifySignature responde válido, inválido o desconocido. Tampoco hay un cliente HTTP integrado para autoridades de sellado de tiempo RFC 3161 — la biblioteca calcula el hash que se enviará y vuelve a incrustar el CMS aumentado, pero el viaje de red pertenece al código de ustedes. Planifiquen ambas piezas en el diseño del banco de trabajo desde el inicio; ambas son fáciles de envolver y muy incómodas de descubrir faltantes en el sprint final.
Preguntas frecuentes
¿Agregar una firma rompe el cumplimiento PDF/A? No por sí solo. La firma llega como una actualización incremental, e ISO 19005-2 en adelante permite explícitamente documentos firmados. Sin embargo, la apariencia de la firma sigue las mismas reglas que cualquier contenido de página — fuentes incrustadas, sin color dependiente del dispositivo — de modo que la última puerta del banco de trabajo debería ser otra ejecución de preflight sobre la salida firmada.
¿Por qué mi archivo pasa aquí pero falla en un validador externo? Los validadores implementan conjuntos de reglas que se solapan, pero no son idénticos. Traten CheckFileCompliance como la puerta rápida dentro del flujo y verifiquen candidatos de release con una herramienta independiente como veraPDF; cuando difieren, el texto del hallazgo suele nombrar la cláusula que hay que leer.
¿Puedo firmar y sellar tiempo en una sola pasada? No — primero se escribe la firma base, luego un proceso separado de sello de tiempo aumenta el CMS dentro del espacio /Contents reservado. Por eso importa la llamada de bytes reservados en el ejemplo de firma; dimensiónenla para el token de sello de tiempo que esperan.
Adónde ir después
Para las capas de sello de tiempo y validación a largo plazo que se construyen sobre este banco de trabajo, consulten el recorrido de firma y validación PAdES. La mitad de preflight se cubre con más profundidad en la guía de preflight PDF/A y PDF/UA.
La documentación completa de la API y las descargas de prueba están en la página del producto PDFlibPas.