Validar una firma PAdES significa comprobar tres elementos independientes, y una marca verde en un visor solo informa sobre el tercero. Primero, el arreglo /ByteRange debe cubrir los bytes correctos: los tramos que indica tienen que reconstruir la entrada exacta sobre la que se calculó el resumen CMS, sin bytes firmados fuera de ellos. Segundo, el certificado dentro del CMS debe encadenarse a una raíz de confianza y llevar el atributo firmado de certificado de firma que exige PAdES. Tercero, si el perfil afirma tener una marca de tiempo, un token RFC 3161 debe vincular el valor de la firma a un momento anterior a la expiración del certificado. Acrobat reduce los tres a un icono; un verificador de conformidad los mantiene separados, y también debería hacerlo el código que produce estos archivos. losLab PDF Library (PDF Library for Delphi) ofrece el lado de firma, la reincrustació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 toda primera implementación de PAdES, así que vale la pena aclararla antes de escribir 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 correcta. Tampoco es una firma PAdES, porque ETSI EN 319 142-1 exige ETSI.CAdES.detached en todos los niveles de referencia. Un verificador 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 hacer esa afirmación correctamente 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 apilados 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 agrega una marca de tiempo RFC 3161 sobre el valor de la firma, que demuestra que la firma existía antes de un momento que nadie puede antedatar. PAdES-B-LT incorpora 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 corona la pila con una marca de tiempo del 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 la política necesita una indicación de tipo de compromiso (prueba de origen, prueba de aprobación o uno de los otros identificadores ETSI numerados del 1 al 6), se indica 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: con el algoritmo de resumen en automático, la biblioteca selecciona SHA-256 para las firmas ETSI y adbe.pkcs7.detached y recurre a SHA-1 solo en la ruta heredada adbe.pkcs7.sha1. Establézcalo explícitamente de todos modos. Los auditores preguntan qué hash se usó, y un valor explícito en el código es más fácil de defender que un valor predeterminado que se debe buscar en el manual para explicar
Producción de la firma de referencia
La API plana controla la firma como una máquina de estados de una sola operación: abre un proceso sobre el archivo de origen, lo configura, finaliza en un archivo de salida y lee el código de resultado. La secuencia siguiente produce 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 se puede cambiar después si alguna vez se debe agregar 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 la fuente no puede abrirse en absoluto. Después de eso, GetSignProcessResult separa los modos de falla que realmente ocurren 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 una falla al aplicar los bytes de firma. Registrar el código numérico junto al nombre del archivo de entrada convierte un ticket de soporte impreciso en un diagnóstico de un minuto
Agregar la marca de tiempo RFC 3161 que la biblioteca no obtendrá por usted
PDF Library for Delphi no incluye cliente TSA, y ese es un límite deliberado, no una omisión. La biblioteca calcula el hash que la autoridad de marcas de tiempo debe contrafirmar y después reincrusta el CMS aumentado; el intercambio HTTP y la cirugía CMS intermedia pertenecen a quien llama. Hay una razón técnica estricta para la separación. El control de Windows CryptoAPI que nominalmente agrega 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 control propio. Ninguna biblioteca puede incorporar silenciosamente el token con una llamada al sistema, y cualquiera que afirme hacerlo está realizando la cirugía en algún lugar que no se 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 tu aplicación: un HTTP POST a tu TSA,
// y un re-encode CMS que adjunta el token como atributo sin firmar
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;
Observe los códigos de resultado aquí: 12 significa que el campo de firma nombrado no existe, 11 que no se pudo analizar el CMS existente y 13 que el CMS aumentado ya no cabe en el marcador de posición /Contents reservado. El código 13 es el que 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 8192 bytes realizada durante el paso B-B existe precisamente para que este paso tenga dónde alojarse
La validación comienza en 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 veredicto estructural sobre el archivo. La validación programática debe empezar más abajo, con la pregunta que las actualizaciones incrementales hacen sutil: ¿qué bytes cubre realmente cada firma? Cada mejora analizada aquí, ya sea una segunda firma, un diccionario DSS o una marca de tiempo del documento, llega mediante una actualización incremental, y cada actualización agrega bytes fuera del /ByteRange de la firma anterior. Esos bytes agregados son legítimos. Un validador aún tiene que clasificarlos frente a la política de modificación del documento, y el nivel DocMDP por campo donde reside esa política 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 significa que realmente está firmado
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 exclusivo de uso compartido, por lo que un validador que también quiera aplicar hash a los bytes del archivo sin procesar para la verificación CMS debe leer el archivo en memoria antes de abrirlo para 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 contraparte de API plana GetSignProcessByteRange devuelve Integer mientras que los desplazamientos subyacentes son Int64, así que después de 2 GB la llamada plana trunca sin avisar, razón por la que este ejemplo obtiene los desplazamientos mediante la clase de auditoría. También vale la pena mencionar una ausencia. La capa plana no tiene ningún envoltorio VerifySignature. Los veredictos criptográficos proceden de TPDFlibSignatureVerifier en el nivel de clase, que devuelve vsValid, vsInvalid o vsUnknown, o de un validador externo en el que la política de conformidad ya confía
Validación a largo plazo: DSS, VRI y la marca de tiempo del 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 de documento que contiene certificados, CRL y respuestas OCSP, indexados opcionalmente por firma mediante entradas VRI codificadas por el hash del /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 de su lado. Obtener el material de revocación y juzgar si es lo bastante reciente como para que valga la pena incrustarlo es tarea de quien llama. La biblioteca garantiza que los diccionarios son estructuralmente conformes; no puede garantizar que su respondedor OCSP dijera la verdad
El punto final de archivo, B-LTA, agrega una marca de tiempo del documento: un campo de firma separado cuyo tipo es DocTimeStamp en vez de Sig, producido mediante SetSignProcessDocTimeStamp con una longitud de firma reservada. No sustituye la marca de tiempo de firma del paso B-T. La marca de tiempo de firma prueba cuándo existía una firma en particular; la marca de tiempo del documento protege todo el archivo, 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 funciones definidas por ETSI
Conviene anticipar una reacción a todo esto, porque parece un error y no lo es. Un visor a menudo informa "validity unknown" en un archivo cuya estructura PAdES es totalmente correcta. La confianza y la estructura son ejes independientes. El visor simplemente no puede encadenar al firmante con una raíz en la que confíe en esa máquina, algo rutinario con CA privadas y certificados de prueba, incluso cuando la auditoría ByteRange y la verificación CMS aprueban. La solución es distribuir correctamente el certificado raíz o evaluar frente a las listas de confianza de la UE cuando el estado eIDAS cualificado es el objetivo real, en vez de tocar el código de firma
Para la perspectiva del lado de auditoría, es decir, enumerar campos de firma en un corpus, volcar diseños ByteRange y leer niveles DocMDP en masa, consulte el artículo complementario sobre el banco de trabajo de conformidad y firma. Los documentos firmados que también deben satisfacer una política de archivo pertenecen al flujo de trabajo descrito en preflight PDF/A y PDF/UA en Delphi. La documentación completa de la API y las descargas de evaluación se encuentran en la página de producto de losLab PDF Library for Delphi