Un banco de trabajo que encadena la validación de cumplimiento a la firma digital tiene que coordinar cuatro pasos, en este orden, y mantenerlos atados a un conjunto de bytes durante todo el camino. Ejecuta una comprobación previa (preflight) de PDF/A o PDF/UA. Aplica las correcciones que exijan los hallazgos y guarda una revisión corregida. Firma esa revisión exacta. Luego lee el archivo firmado y confirma que la firma realmente lo cubre. El orden no es cosmético. Omita la relectura y estará confiando en su propia ruta de escritura; deje que la comprobación previa se ejecute frente a la revisión incorrecta y su informe de cumplimiento describirá un archivo que nunca envió
La parte en la que la mayoría de las canalizaciones (pipelines) de cosecha propia se equivocan es en la costura entre la validación y la firma. Ejecútelos como dos herramientas separadas con un pase de corrección en el medio y surgirán al menos tres revisiones distintas del archivo, cada una con sus propios bytes. El informe de comprobación previa que entrega a un auditor describe uno de ellos. La firma congela otro. Nada en el archivo indica que sean la misma revisión y, a menudo, no lo son. PDFlibPas, la losLab PDF Developer Library para Delphi y C++Builder, coloca la comprobación previa y la firma PAdES detrás de una única clase de fachada, por lo que toda la secuencia puede vivir en un proceso que nunca pierde de vista de qué bytes está hablando. Cada llamada a continuación existe en la biblioteca hoy en día, al igual que cada trampa anotada junto a ella
Tres revisiones de un documento, y cómo se abre la brecha
Cuente los guardados. El original llega desde arriba (upstream). El pase de remediación lo carga, activa un modo de cumplimiento y escribe una revisión corregida. El pase de firma añade una firma como una actualización incremental, que es una tercera escritura. Tres guardados, tres diseños de bytes y un informe de comprobación previa no significan nada a menos que nombre cuál de los tres cubre. Un SHA-256 del archivo, registrado junto a cada ejecución de comprobación previa y cada firma, es el ancla barata que le permite probar que la revisión que validó es la revisión que firmó
Un comportamiento de la biblioteca refuerza aún más esa disciplina. Las correcciones de cumplimiento solicitadas a través de SetPDFAMode o SetPDFUAMode no surten efecto cuando las llama. Se aplican durante el guardado. Las reparaciones automáticas, como forzar las banderas de impresión de anotaciones o asignar un orden de pestañas de PDF/UA, aterrizan en el archivo de salida y en ningún otro lugar, por lo que una comprobación ejecutada frente al documento que acaba de "arreglar" en la memoria no le dice nada sobre los bytes que se dirigen al firmante. Guarde primero y luego realice la comprobación previa del archivo guardado. El estado en memoria es un borrador; solo el archivo en el disco es real
Comprobación previa (preflight) desde disco, y el cero que significa dos cosas
El punto de entrada plano de comprobación previa es CheckFileCompliance(FileName, Password, ComplianceTest, Options). La prueba 1 selecciona PDF/A (ISO 19005), la prueba 2 selecciona PDF/UA (ISO 14289). Abre el archivo a través del lector de transmisión (streaming) de la biblioteca, por lo que no hay necesidad de LoadFromFile primero, y devuelve un controlador de lista de cadenas que lleva un hallazgo por entrada:
La trampa se encuentra en el valor de retorno, y es del tipo que aprueba todas las pruebas de ruta feliz. Cero significa "no hay hallazgos". Cero también significa "el archivo no se pudo abrir", porque la implementación devuelve 0 siempre que la lista de resultados vuelve vacía, lo que incluye un fallo de lectura. Un banco de trabajo que lea 0 como luz verde aprobará alegremente un archivo que algún otro proceso haya bloqueado. Emparejar la llamada con LastErrorCode, como arriba, es lo que separa los dos casos. El comprobador también abre el archivo con un modo de uso compartido de denegación de escritura, por lo que si su paso de corrección todavía contiene un identificador de escritor, la comprobación previa falla por una razón que no tiene nada que ver con el cumplimiento y todo que ver con una transmisión (stream) que olvidó liberar
Cuando una persona, en lugar de una canalización, necesita leer los hallazgos, CreatePreflightReport los representa como un informe legible. ComparePreflightReports diferencia (diffs) dos ejecuciones, lo que es una forma ordenada de mostrar que la corrección eliminó los hallazgos originales sin introducir silenciosamente otros nuevos
Firmar la revisión comprobada con un SignProcess
Una vez que la revisión guardada pasa la comprobación previa y su hash está registrado, firme ese archivo exacto y no otro. La API de SignProcess se lee como un constructor (builder). Abra un controlador de proceso, configúrelo línea por línea, confirme y luego lea 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 en esa secuencia tienen más peso del que parece. SetSignProcessCustomSubFilter con ETSI.CAdES.detached elige una firma PAdES tal y como se describe en ETSI EN 319 142-1 en lugar de la familia heredada adbe.pkcs7.detached, que es la diferencia entre una firma que un validador europeo acepta y una que marca. SetSignProcessReserveContentsBytes rellena el marcador de posición /Contents, y el tamaño que elija aquí es una decisión sobre el futuro: si alguna vez le va a seguir una marca de tiempo de firma, el CMS ampliado tiene que ajustarse al espacio que reserve ahora, porque el marcador de posición no puede crecer más tarde sin volver a firmar todo el asunto. Reserve generosamente y desperdiciará unos pocos kilobytes. Reserve demasiado ajustado y el paso de la marca de tiempo fallará dentro de meses con un desbordamiento que le costará relacionar con esta única línea
GetSignProcessResult responde con un código, no con un valor booleano, y vale la pena conservar los códigos. 1 es éxito. 4 es una contraseña de PDF incorrecta, 7 una contraseña de certificado incorrecta, 9 un PFX que no lleva clave privada, 11 un fallo mientras se aplicaba la firma. Si los colapsa en un verdadero/falso, tirará a la basura la única pieza de información que distingue un caso de soporte de contraseña incorrecta de uno de clave sin parte privada. Registre el número entero
Relectura: auditoría del archivo que acaba de producir
Ningún banco de trabajo debería confiar en la ruta que escribió el archivo que está a punto de certificar. La clase de auditoría TPDFlibSignDoc vuelve a abrir la salida firmada y lee las entradas del diccionario de firmas directamente del disco:
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 las entradas del diccionario. La clave 0 devuelve el CMS sin procesar de /Contents, las claves 2 y 3 los nombres /Filter y /SubFilter, y de 11 a 14 los cuatro números ByteRange. En cambio, los valores de texto regresan a través de GetSignatureTextValueByName: la clave 0 es la hora de firma reclamada, y la clave 5 distingue un Sig normal de un DocTimeStamp, lo cual importa una vez que un documento lleva ambos
La captura del tamaño del archivo en la parte superior de ese ejemplo es un elemento de carga (load-bearing), no una tarea de limpieza. TPDFlibSignDoc.Open retiene el archivo bajo un bloqueo de uso compartido restrictivo durante toda su vida útil, por lo que todo lo que necesite los bytes sin procesar (aplicar hash al rango firmado, volver a calcular el resumen CMS) tiene que leer el archivo antes de llamar a Open. La propia demostración SigningWorkbench de la biblioteca lee primero todo el archivo en la memoria precisamente por esta razón, y un banco de trabajo que ignora el orden falla intermitentemente, en la máquina que pierda la carrera
Aritmética de ByteRange que prueba la cobertura
Un archivo de una sola firma en buen estado tiene un ByteRange de la forma [0 a b c]: la cobertura comienza en el desplazamiento 0, se salta el marcador de posición hexadecimal /Contents entre a y b, luego se reanuda a través del byte b+c. Cuando b+c es igual al tamaño del archivo, la firma cubre todo hasta el final del archivo, que es el resultado que desea. Cuando se queda corto, alguien ha añadido una actualización incremental después de escribir la firma. Eso es perfectamente legítimo según ISO 32000-1§12.8, ya que los rellenos de formularios posteriores, una segunda firma y un diccionario DSS llegan exactamente de esta manera. También es precisamente el hecho que un rastro de auditoría debería registrar en el momento de la firma en lugar de reconstruirse bajo presión durante una disputa
Observe la anchura del número entero mientras realiza esta aritmética. El GetSignProcessByteRange de la API plana devuelve un Integer de 32 bits, pero los valores subyacentes son Int64, por lo que en un archivo de más de 2 GB el descriptor de acceso plano trunca silenciosamente. Recurra a la capa de clase TPDFlibSigner.GetByteRange, que devuelve Int64, o analice los valores de GetSignatureValueByName de la forma en que lo hace el código de auditoría anterior
Lo que la biblioteca le deja a usted
Es mejor aprender dos límites en el tiempo de diseño que en el sprint final. La API plana de TPDFlib no incluye ningún envoltorio (wrapper) de verificación de firma en absoluto. La verificación criptográfica vive una capa más abajo, en TPDFlibSignatureVerifier, cuyo VerifySignature responde válido, no válido o desconocido. Tampoco hay un cliente HTTP incorporado para las autoridades de marcas de tiempo RFC 3161. La biblioteca calcula el hash a enviar y vuelve a incrustar el CMS aumentado una vez que regresa un token, pero el viaje de ida y vuelta a la TSA en la red es suyo para escribirlo. Ambos son fáciles de envolver y genuinamente desagradables de descubrir que faltan la semana anterior a un lanzamiento, así que diséñelos desde el primer boceto
Vale la pena resolver una pregunta sobre el cumplimiento sin rodeos, porque decide dónde va la última puerta: ¿agregar una firma rompe PDF/A? No por sí sola. La firma llega como una actualización incremental, y de ISO 19005-2 en adelante se permiten explícitamente los documentos firmados. El truco es la apariencia de la firma, que se rige por las mismas reglas que cualquier otro contenido de la página, incluidas las fuentes incrustadas y la ausencia de color dependiente del dispositivo. Por lo tanto, la última puerta en el banco de trabajo es una ejecución de comprobación previa más, esta vez frente a la salida firmada. Trate a CheckFileCompliance como la comprobación rápida en curso (in-pipeline) y aún así verifique las candidatas de lanzamiento con una herramienta independiente como veraPDF, ya que los validadores implementan conjuntos de reglas superpuestos pero no idénticos; cuando los dos no están de acuerdo, el texto del hallazgo suele nombrar la cláusula que hay que ir a leer
Un punto de secuenciación se deduce de todo esto. La firma y el sellado de tiempo no son un solo paso: la firma base se escribe primero, luego un proceso de marca de tiempo separado aumenta el CMS dentro del espacio /Contents reservado, que es exactamente por lo que la línea de bytes de reserva tenía tanto peso antes. Para las capas de validación a largo plazo y la marca de tiempo que se basan en este banco de trabajo, el recorrido por la validación y firma de PAdES lleva la firma de la línea base a B-LT, y la mitad de la comprobación previa profundiza en la guía de comprobación previa de PDF/A y PDF/UA. La documentación completa de la API y las descargas de prueba se encuentran en la página del producto PDFlibPas
Ejemplos de código adicionales
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;