Adobe Acrobat reportaba como válida cada firma del lote. El verificador de conformidad eIDAS del cliente las rechazaba todas. La brecha se reducía a un solo nombre en el diccionario de firma: los archivos llevaban /SubFilter /adbe.pkcs7.detached, que produce una firma ISO 32000-1 §12.8 perfectamente sólida y una firma PAdES no conforme, porque ETSI EN 319 142-1 requiere ETSI.CAdES.detached en cada nivel baseline. La criptografía era impecable; el documento simplemente no declaraba el perfil que exigía el regulador. Si su aplicación Delphi firma facturas, contratos o reportes de laboratorio que deben sobrevivir políticas de validación estilo europeo, esta distinción es lo primero que deben corregir — y es una llamada en losLab PDF Library (PDFlibPas), cuya cadena de herramientas de firma, sellado de tiempo y DSS recorre este artículo desde la firma baseline hasta la validación a largo plazo.
Qué convierte una firma PDF en una firma PAdES
ETSI EN 319 142-1 define cuatro niveles baseline apilados sobre el formato CMS. PAdES-B-B es el punto de entrada: una firma CAdES en un campo de firma PDF con SubFilter ETSI.CAdES.detached y un atributo firmado de certificado de firma. PAdES-B-T agrega un sello de tiempo RFC 3161 sobre el valor de la firma, probando que la firma existía antes de un punto en el tiempo que nadie puede retrofechar. PAdES-B-LT incrusta los certificados, CRLs y respuestas OCSP necesarios para la validación en un Document Security Store, de modo que el archivo siga siendo verificable después de que la CA emisora retire su infraestructura. PAdES-B-LTA corona la pila con un sello de tiempo de documento que vuelve a proteger la evidencia acumulada cuando los algoritmos se debilitan.
PDFlibPas asigna estos conceptos a su API sign-process. El marcador de perfil es SetSignProcessCustomSubFilter; las indicaciones de tipo de compromiso ETSI — prueba de origen, prueba de aprobación y los demás identificadores definidos por ETSI del 1 al 6 — pasan por SetSignProcessCommitmentType; una política de firma explícita se adjunta con SetSignProcessSignaturePolicy, que toma el OID de la política y su digest. Un valor predeterminado merece atención: con el algoritmo digest en auto, la biblioteca selecciona SHA-256 para firmas ETSI y adbe.pkcs7.detached, y solo cae a SHA-1 en la ruta heredada adbe.pkcs7.sha1. Configúrenlo explícitamente de todos modos; los auditores preguntan, y lo explícito vence a lo inferido en toda revisión de cumplimiento.
Producción de la firma baseline
La API plana conduce la firma como una máquina de estados de una sola pasada: abrir un proceso sobre el archivo fuente, configurarlo, terminar hacia un archivo de salida y leer el código de resultado. La secuencia siguiente produce una firma PAdES-B-B con SHA-256 — y reserva deliberadamente espacio adicional dentro del marcador /Contents, que es la línea que no podrán agregar más tarde si alguna vez se necesita un sello de tiempo para esta firma.
var
Pdf: TPDFlib;
SignId: Integer;
begin
Pdf := TPDFlib.Create;
try
SignId := Pdf.NewSignProcessFromFile('invoice.pdf', '');
if SignId = 0 then
raise Exception.Create('cannot open source PDF');
Pdf.SetSignProcessField(SignId, 'Sig1');
Pdf.SetSignProcessPFXFromFile(SignId, 'company.pfx', PfxPassword);
Pdf.SetSignProcessInfo(SignId, 'Approved', 'Vienna', 'billing@example.com');
Pdf.SetSignProcessCustomSubFilter(SignId, 'ETSI.CAdES.detached');
Pdf.SetSignProcessDigestAlgorithm(SignId, 2); // SHA-256
Pdf.SetSignProcessReserveContentsBytes(SignId, 8192); // room for a timestamp later
Pdf.EndSignProcessToFile(SignId, 'invoice-signed.pdf');
if Pdf.GetSignProcessResult(SignId) <> 1 then
raise Exception.CreateFmt('signing failed, code %d',
[Pdf.GetSignProcessResult(SignId)]);
Pdf.ReleaseSignProcess(SignId);
finally
Pdf.Free;
end;
end;
NewSignProcessFromFile devuelve 0 cuando la fuente no puede abrirse en absoluto. Después de eso, GetSignProcessResult separa los modos de fallo que realmente ocurren en producción: 4 significa contraseña PDF incorrecta, 7 contraseña PFX incorrecta, 9 un archivo de certificado sin clave privada, 10 una ruta de salida no escribible, 11 un fallo al aplicar los bytes de firma. Registrar el código numérico junto al nombre del archivo de entrada convierte un ticket de soporte vago en un diagnóstico de un minuto.
Agregar el sello de tiempo RFC 3161 que la biblioteca no buscará por ustedes
PDFlibPas no incluye cliente TSA, y ese es un límite deliberado, no una omisión. La biblioteca calcula el hash que la autoridad de sellado de tiempo debe contrafirmar y vuelve a incrustar después el CMS aumentado; el intercambio HTTP y la cirugía CMS intermedia pertenecen al llamador. Hay una razón técnica dura para esa separación: el control Windows CryptoAPI que nominalmente agrega atributos no autenticados, CMSG_CTRL_ADD_SIGNER_UNAUTH_ATTR, falla con CRYPT_E_INVALID_INDEX en el diseño detached SignedData que usa PAdES, así que el CMS mejorado debe venir de un codificador CMS bajo control de ustedes — ninguna biblioteca puede hacerlo silenciosamente con una sola llamada de sistema.
var
Pdf: TPDFlib;
StsId: Integer;
HashHex, TstDer, TsAttr, AugmentedCms: AnsiString;
begin
Pdf := TPDFlib.Create;
try
StsId := Pdf.NewPAdESSignatureTimeStampProcessFromFile('invoice-signed.pdf', '');
Pdf.SetPAdESSignatureTimeStampField(StsId, 'Sig1');
Pdf.SetPAdESSignatureTimeStampDigestAlgorithm(StsId, 2);
HashHex := Pdf.GetPAdESSignatureValueHashHex(StsId);
// both calls below are application code: an HTTP POST to your TSA,
// and a CMS re-encode that attaches the token as an unsigned attribute
TstDer := RequestTimeStampToken(HashHex);
TsAttr := Pdf.BuildPAdESSignatureTimeStampAttribute(TstDer);
AugmentedCms := AttachUnsignedAttribute(Pdf.GetPAdESSignatureCMSBytes(StsId), TsAttr);
Pdf.SetPAdESSignatureCMSBytes(StsId, AugmentedCms);
Pdf.EndPAdESSignatureTimeStampProcessToFile(StsId, 'invoice-bt.pdf');
if Pdf.GetPAdESSignatureTimeStampProcessResult(StsId) <> 1 then
raise Exception.Create('timestamp embedding failed');
Pdf.ReleasePAdESSignatureTimeStampProcess(StsId);
finally
Pdf.Free;
end;
end;
Observen los códigos de resultado aquí: 12 significa que el campo de firma nombrado no existe, 11 que el CMS existente no pudo analizarse, y 13 que el CMS aumentado ya no cabe en el marcador /Contents reservado. El código 13 es el doloroso, porque la única corrección es volver a firmar: un token de sello de tiempo típico con su cadena de certificados pesa de 4 a 6 KB, y la reserva de 8192 bytes hecha durante el paso B-B existe precisamente para que este paso tenga espacio donde aterrizar.
La validación empieza en ByteRange, no en la cadena de certificados
Una marca verde en un visor es una decisión de confianza contra el almacén de certificados de esa máquina, no un veredicto estructural sobre el archivo. La validación programática debe empezar más abajo, con la pregunta que las actualizaciones incrementales vuelven sutil: ¿qué bytes cubre realmente cada firma? Cada mejora tratada aquí — segundas firmas, diccionarios DSS, sellos de tiempo de documento — llega mediante actualización incremental, y cada actualización agrega bytes fuera del /ByteRange de la firma anterior. Esos bytes agregados son legítimos, pero un validador debe clasificarlos contra la política de modificación del documento; el nivel DocMDP por campo se puede leer con GetSignatureDocMDPLevelByName.
var
Doc: TPDFlibSignDoc;
Names: TStringList;
I: Integer;
B0, B1, B2, B3, FileSize: Int64;
begin
FileSize := TFile.GetSize('invoice-bt.pdf'); // before Open: SignDoc holds a share lock
Doc := TPDFlibSignDoc.Create;
try
if not Doc.Open('invoice-bt.pdf', '', False) then
raise Exception.Create('cannot open for audit');
Names := TStringList.Create;
try
Doc.GetSignatureFieldNames(Names);
for I := 0 to Names.Count - 1 do
if Doc.GetSignatureValueObjNum(Names[I]) > 0 then // >0 means actually signed
begin
B0 := StrToInt64(string(Doc.GetSignatureValueByName(Names[I], 11)));
B1 := StrToInt64(string(Doc.GetSignatureValueByName(Names[I], 12)));
B2 := StrToInt64(string(Doc.GetSignatureValueByName(Names[I], 13)));
B3 := StrToInt64(string(Doc.GetSignatureValueByName(Names[I], 14)));
if (B0 = 0) and (B2 + B3 = FileSize) then
Writeln(Names[I], ': covers the file to EOF')
else
Writeln(Names[I], ': earlier revision, or unexpected ByteRange layout');
end;
finally
Names.Free;
end;
Doc.Close;
finally
Doc.Free;
end;
end;
Dos trampas viven en esta ruta de auditoría. TPDFlibSignDoc.Open mantiene el archivo con un bloqueo de uso compartido exclusivo, así que un validador que también quiera calcular hash de bytes crudos para verificar CMS debe leer el archivo en memoria antes de abrirlo para auditoría — el orden importa. Y la contraparte de API plana GetSignProcessByteRange devuelve Integer mientras que los offsets subyacentes son Int64: más allá de 2 GB, la llamada plana trunca silenciosamente, por eso este ejemplo obtiene offsets mediante la clase de auditoría. Noten también lo que está ausente deliberadamente en la capa plana: no hay ningún wrapper VerifySignature. Los veredictos criptográficos vienen de la clase TPDFlibSignatureVerifier, que devuelve vsValid, vsInvalid o vsUnknown, o de un validador externo que su política de cumplimiento ya confíe.
Validación a largo plazo: DSS, VRI y el sello de tiempo de documento
PAdES-B-LT existe porque la infraestructura de revocación es mortal. ETSI EN 319 142-1 §5.4.2.2 especifica el Document Security Store: un diccionario de nivel documento que contiene certificados, CRLs y respuestas OCSP, opcionalmente indexado por firma mediante entradas VRI con clave en el hash del /Contents de cada firma. El flujo de PDFlibPas refleja el diseño del sello de tiempo: NewPAdESDSSProcessFromFile abre el proceso; AddPAdESDSSCertificate, AddPAdESDSSCRL y AddPAdESDSSOCSP aceptan blobs DER; AddPAdESDSSVRI vincula material seleccionado a una firma; EndPAdESDSSProcessToFile escribe todo como actualización incremental. Obtener el material de revocación — y juzgar si es suficientemente fresco para incrustarlo — sigue siendo responsabilidad del llamador; la biblioteca garantiza que los diccionarios sean estructuralmente conformes, no que el respondedor OCSP haya dicho la verdad.
El punto final archivístico, B-LTA, agrega un sello de tiempo de documento: un campo de firma separado cuyo tipo es DocTimeStamp en lugar de Sig, producido mediante SetSignProcessDocTimeStamp con una longitud de firma reservada. Para lectores anteriores a estas estructuras, TPDFlibSignDoc.EnsurePAdESExtensions registra la extensión de desarrollador ESIC en el catálogo del documento, anunciando que el archivo usa características definidas por ETSI.
Preguntas frecuentes
¿Por qué Acrobat dice "validez desconocida" cuando la estructura PAdES es correcta?
Porque confianza y estructura son independientes. El visor no puede encadenar el firmante hacia una raíz que confíe en esa máquina — algo común con CAs privadas y certificados de prueba — mientras que la auditoría ByteRange y la verificación CMS pasan al mismo tiempo. Distribuyan correctamente el certificado raíz, o evalúen contra las listas de confianza de la UE cuando la calificación eIDAS sea el objetivo real.
¿Se puede agregar un sello de tiempo a una firma que no reservó espacio extra en Contents?
Por lo general no. El CMS aumentado debe caber en el marcador original, y un marcador de tamaño predeterminado ajusta la firma original de forma estrecha. Esperen el código de resultado 13 y planifiquen volver a firmar con SetSignProcessReserveContentsBytes desde el inicio.
¿Un sello de tiempo de documento reemplaza el sello de tiempo de firma?
No. El sello de tiempo de firma prueba cuándo existía una firma; el sello de tiempo de documento protege todo el archivo, incluida su evidencia DSS, y es el elemento que se renueva a lo largo de décadas. Los perfiles archivísticos terminan cargando ambos.
Para la perspectiva del lado de auditoría — enumerar campos de firma en un corpus, volcar diseños ByteRange y leer niveles DocMDP en lote — consulten la pieza complementaria sobre el banco de trabajo de cumplimiento y firmas. Los documentos firmados que también deben satisfacer políticas archivísticas pertenecen al flujo descrito en preflight PDF/A y PDF/UA en Delphi. La documentación completa de la API y las descargas de evaluación están en la página del producto losLab PDF Library for Delphi.