Validar una firma PAdES significa comprobar tres cosas independientes, y una marca verde en un visor solo le informa sobre la tercera. En primer lugar, la matriz /ByteRange debe cubrir los bytes correctos: los intervalos que nombra han de reconstruir la entrada exacta sobre la que se calculó el resumen CMS, sin bytes firmados fuera de ellos. En segundo lugar, el certificado dentro del CMS debe encadenarse a una raíz en la que confíe e incluir el atributo firmado de certificado de firma que exige PAdES. En tercer lugar, si el perfil declara una marca de tiempo, un token RFC 3161 debe vincular el valor de la firma a un momento anterior a la caducidad del certificado. Acrobat reúne las tres cosas en un único icono; un comprobador de conformidad las mantiene separadas, y también debería hacerlo el código que produce estos archivos. losLab PDF Library (PDF Library for Delphi) proporciona la parte de firma, la reinserción de marcas de tiempo y las llamadas de auditoría para inspeccionar un ByteRange antes de confiar en él
Una distinción confunde casi todas las primeras implementaciones de PAdES, por lo que conviene exponerla antes de mostrar código. Una firma escrita con /SubFilter /adbe.pkcs7.detached es una firma ISO 32000-1 §12.8 perfectamente válida que Acrobat informará como válida. Sin embargo, no es una firma PAdES, porque ETSI EN 319 142-1 exige ETSI.CAdES.detached en todos los niveles de referencia. Un comprobador de conformidad eIDAS rechaza la primera y acepta la segunda aunque la criptografía sea idéntica. El perfil es una afirmación que el documento hace sobre sí mismo, y acertar con esa afirmación es una llamada en PDF Library for Delphi
Qué convierte una firma PDF en una firma PAdES
ETSI EN 319 142-1 define cuatro niveles de referencia superpuestos sobre el formato CMS. PAdES-B-B es el punto de entrada: una firma CAdES en un campo de firma PDF con el SubFilter ETSI.CAdES.detached y un atributo firmado de certificado de firma. PAdES-B-T añade una marca de tiempo RFC 3161 sobre el valor de firma, que prueba que la firma existía antes de un momento que nadie puede fechar hacia atrás. PAdES-B-LT incrusta los certificados, CRL 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 culmina la pila con una marca de tiempo de documento que vuelve a proteger la evidencia acumulada a medida que los algoritmos se debilitan
PDF Library for Delphi asigna estos conceptos a su API de proceso de firma. El marcador de perfil es SetSignProcessCustomSubFilter. Si su política necesita una indicación de tipo de compromiso (prueba de origen, prueba de aprobación u otro de los identificadores ETSI numerados del 1 al 6), se establece mediante SetSignProcessCommitmentType. Una política de firma explícita se adjunta con SetSignProcessSignaturePolicy, que recibe el OID de la política y su resumen. Un valor predeterminado merece atención: si se deja automático el algoritmo de resumen, la biblioteca selecciona SHA-256 para firmas ETSI y adbe.pkcs7.detached, y solo recurre a SHA-1 en la ruta heredada adbe.pkcs7.sha1. Establézcalo explícitamente de todos modos. Los auditores preguntan qué hash utilizó, y un valor explícito en el código es más fácil de defender que un valor predeterminado cuya explicación exige consultar el manual
Producir la firma de referencia
La API plana controla la firma como una máquina de estados de una sola ejecución: abre un proceso sobre el archivo de origen, lo configura, lo termina en un archivo de salida y lee el código de resultado. La secuencia siguiente genera una firma PAdES-B-B con SHA-256. La línea más importante no tiene nada que ver con la firma en sí. Es la reserva deliberadamente sobredimensionada de /Contents, porque es lo único que no podrá cambiar más adelante si alguna vez necesita añadir una marca de tiempo a 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 el origen no puede abrirse en absoluto. Después, GetSignProcessResult distingue los modos de fallo que realmente aparecen en producción: 4 significa una contraseña PDF incorrecta, 7 una contraseña PFX incorrecta, 9 un archivo de certificado sin clave privada, 10 una ruta de salida no escribible y 11 un fallo al aplicar los bytes de firma. Registrar el código numérico junto al nombre del archivo de entrada convierte una incidencia de soporte imprecisa en un diagnóstico de un minuto
Añadir la marca de tiempo RFC 3161 que la biblioteca no obtiene por usted
PDF Library for Delphi no incluye un cliente TSA, y ese es un límite deliberado, no una carencia. La biblioteca calcula el hash que la autoridad de sellado de tiempo debe contrafirmar y después reincrusta el CMS aumentado; el intercambio HTTP y la manipulación CMS intermedia corresponden al llamador. Hay una razón técnica contundente para esta división. El control CryptoAPI de Windows que nominalmente añade atributos sin firmar, CMSG_CTRL_ADD_SIGNER_UNAUTH_ATTR, falla con CRYPT_E_INVALID_INDEX en el diseño SignedData separado que usa PAdES. Por ello, el CMS mejorado debe proceder de un codificador CMS bajo su propio control. Ninguna biblioteca puede integrar silenciosamente el token con una sola llamada al sistema, y cualquiera que afirme hacerlo está realizando la manipulación en algún lugar que usted no puede ver
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);
// las dos llamadas de abajo son código de aplicación: un POST HTTP a tu TSA,
// y una recodificación CMS que adjunta el token como atributo unsigned
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;
Vigile aquí los códigos de resultado: 12 significa que el campo de firma indicado no existe, 11 que el CMS existente no pudo analizarse y 13 que el CMS aumentado ya no cabe en el marcador de posición /Contents reservado. El código 13 es el que más duele, porque la única solución es volver a firmar: un token de marca de tiempo típico con su cadena de certificados ocupa de 4 a 6 KB, y la reserva de 8.192 bytes realizada durante el paso B-B existe precisamente para que este paso tenga espacio donde asentarse
La validación empieza en el ByteRange, no en la cadena de certificados
Una marca verde en un visor es una decisión de confianza frente al almacén de certificados de esa máquina, no un dictamen estructural sobre el archivo. La validación programática debe empezar más abajo, con una pregunta que las actualizaciones incrementales hacen sutil: ¿qué bytes cubre realmente cada firma? Cada mejora tratada aquí, ya sea una segunda firma, un diccionario DSS o una marca de tiempo de documento, llega mediante una actualización incremental y cada actualización añade bytes fuera del /ByteRange de la firma anterior. Esos bytes añadidos son legítimos. Aun así, un validador debe clasificarlos según la política de modificación del documento, y el nivel DocMDP por campo donde reside esa política puede leerse 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 significa que está firmado de verdad
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;
Hay dos trampas en esta ruta de auditoría. TPDFlibSignDoc.Open mantiene el archivo con un bloqueo de compartición exclusivo, de modo que un validador que también quiera calcular el hash de los bytes sin procesar para la verificación CMS debe leer el archivo en memoria antes de abrirlo para la auditoría. Invierta ese orden y la lectura falla por un bloqueo que usted mismo estableció. La segunda trampa es silenciosa en lugar de ruidosa: la equivalente de API plana GetSignProcessByteRange devuelve Integer, mientras los desplazamientos subyacentes son Int64, por lo que más allá de 2 GB la llamada plana se trunca sin avisar; por ello este ejemplo obtiene los desplazamientos mediante la clase de auditoría. También conviene mencionar una ausencia. La capa plana no tiene ningún envoltorio VerifySignature. Los dictámenes criptográficos proceden del TPDFlibSignatureVerifier de nivel de clase, que devuelve vsValid, vsInvalid o vsUnknown, o de un validador externo en el que ya confíe su política de cumplimiento
Validación a largo plazo: DSS, VRI y la marca 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 a nivel de documento que contiene certificados, CRL y respuestas OCSP, opcionalmente indexadas por firma mediante entradas VRI cuya clave es el hash de /Contents de cada firma. El flujo de PDF Library for Delphi refleja el diseño de la marca de tiempo. NewPAdESDSSProcessFromFile abre el proceso; AddPAdESDSSCertificate, AddPAdESDSSCRL y AddPAdESDSSOCSP aceptan blobs DER; AddPAdESDSSVRI vincula material seleccionado a una firma; EndPAdESDSSProcessToFile escribe todo como una actualización incremental. La parte difícil sigue estando de su lado. Obtener el material de revocación y decidir si está lo bastante actualizado para que merezca la pena incrustarlo es tarea del llamador. La biblioteca garantiza que los diccionarios son estructuralmente conformes; no puede garantizar que su respondedor OCSP haya dicho la verdad
El extremo de archivo, B-LTA, añade una marca de tiempo de documento: un campo de firma independiente cuyo tipo es DocTimeStamp en lugar de Sig, generado mediante SetSignProcessDocTimeStamp con una longitud de firma reservada. No sustituye a la marca de tiempo de firma del paso B-T. La marca de tiempo de firma demuestra cuándo existía una firma concreta; la marca de tiempo de documento protege el archivo completo, incluida la evidencia DSS, y es el elemento que un archivo a largo plazo renueva cada pocos años a medida que los algoritmos se debilitan. Un perfil de archivo maduro incluye ambas. 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 utiliza funcionalidades definidas por ETSI
Conviene prevenir una reacción a todo esto, porque parece un error y no lo es. Un visor a menudo informa «validez desconocida» para un archivo cuya estructura PAdES es completamente correcta. La confianza y la estructura son ejes independientes. El visor simplemente no puede encadenar el firmante a una raíz en la que confíe esa máquina, algo habitual con CA privadas y certificados de prueba, aunque tanto la auditoría ByteRange como la verificación CMS sean satisfactorias. La solución es distribuir correctamente el certificado raíz o evaluar frente a las listas de confianza de la UE cuando el objetivo real sea la condición eIDAS cualificada, no modificar el código de firma
Para la perspectiva de auditoría, es decir, enumerar campos de firma en un conjunto de documentos, volcar diseños ByteRange y leer niveles DocMDP en bloque, consulte el artículo complementario sobre el banco de trabajo de cumplimiento y firma. Los documentos firmados que también deben cumplir una política de archivo pertenecen al flujo de trabajo descrito en la comprobación previa 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 de producto losLab PDF Library para Delphi