Usted ha recibido un PDF firmado y necesita mostrar, en su visor, quién lo firmó, cuándo se firmó, si la firma cubre todo el archivo y qué tan cerca está de cumplir con las normas de conservación a largo plazo. El PDFium Component para Delphi y Lazarus responde a las cuatro preguntas con llamadas de solo lectura: FPDF_GetSignatureCount y la familia FPDFSignatureObj_* exponen el diccionario de la firma, y TPdf.ValidatePades clasifica el nivel base de PAdES. Este es el primero de tres artículos sobre firmas PDF con PDFium; los dos siguientes cubren la creación de una firma B-B y la adición de marcas de tiempo a largo plazo. Sin embargo, hay un límite que debe definirse de antemano: todo lo que se explica aquí es inspección, y leer lo que declara una firma es una tarea muy distinta a verificar su criptografía o decidir si confía en el firmante
Por qué un diccionario de firma PDF no es solo un bloque de bytes
Una firma PDF es un diccionario, no un archivo adjunto opaco, y sus dos entradas más importantes le indican qué parte del archivo está realmente protegida. La norma ISO 32000-1 §12.8 define el diccionario de firma con una entrada /ByteRange y otra /Contents. /Contents contiene una estructura CMS SignedData codificada en hexadecimal (RFC 5652), el contenedor criptográfico que transporta el certificado del firmante, los atributos firmados y el valor de la firma en sí. /ByteRange es la parte que los desarrolladores suelen subestimar: es un arreglo de dos intervalos de desplazamiento y longitud que juntos cubren todo el archivo, a excepción de la cadena hexadecimal de /Contents. Esa brecha es exactamente donde se ubican los bytes de la firma, y los dos intervalos a cada lado son precisamente lo que la firma respalda
El diseño de ByteRange es lo que hace que un guardado incremental sea auditable. Dado que un firmante no puede calcular el hash de los bytes de la firma que aún no existen, el archivo se divide alrededor del marcador de posición /Contents y todo lo demás se procesa en el hash de la firma. Una firma cuyo ByteRange no llegue al final del archivo es una señal de advertencia: el contenido añadido después del rango cubierto, mediante una actualización incremental posterior, no rompería la firma aunque cambie lo que ve el lector. Por lo tanto, lo primero que comprueba un inspector riguroso no es quién firmó, sino si la firma cubre los bytes que aparenta respaldar
Leer el diccionario de firma con la API de solo lectura de PDFium
El PDFium Component expone el diccionario de firma a través de dos miembros de solo lectura: SignatureCount y el registro Signature[Index]. Internamente, llaman a FPDF_GetSignatureCount, FPDF_GetSignatureObject y a los métodos de acceso de la familia FPDFSignatureObj_* para obtener /SubFilter, /ByteRange, /Contents, /Reason y la hora de la firma. "Solo lectura" es la frase clave aquí: PDFium puede enumerar y leer firmas pero no cuenta con una API para crear o escribir una, razón por la cual la parte de firma de esta serie es implementada por la propia biblioteca en lugar de PDFium
var
Pdf: TPdf;
i: Integer;
Sig: TPdfSignature;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := 'contract-signed.pdf';
Pdf.Active := True;
for i := 0 to Pdf.SignatureCount - 1 do
begin
Sig := Pdf.Signature[i];
Writeln('SubFilter : ', Sig.Encoding); // ETSI.CAdES.detached, adbe.pkcs7.detached, ...
Writeln('Signed at : ', Sig.Time); // signer date string, e.g. D:20260708120000+02'00'
Writeln('Reason : ', Sig.Reason);
Writeln('CMS length: ', Length(Sig.Content)); // raw DER SignedData taken from /Contents
Writeln('DocMDP : ', Sig.Permission); // 0 = no certification, 1..3 = MDP level
end;
finally
Pdf.Free;
end;
end;
¿Cuál es la diferencia entre PAdES B-B, B-T, B-LT y B-LTA?
Los cuatro niveles base de PAdES forman una escala desde una firma mínimamente válida hasta una diseñada para sobrevivir décadas de archivo, y cada nivel contiene estrictamente al anterior. La norma ETSI EN 319 142-1 los define como B-B, B-T, B-LT y B-LTA. B-B (Básico) es la firma más los atributos firmados obligatorios y nada más. B-T (Marca de tiempo / Timestamp) añade una marca de tiempo confiable RFC 3161 timestamp sobre la firma, por lo que el momento de la firma es certificado por una autoridad de sellado de tiempo en lugar de ser declarado por el reloj del firmante. B-LT (Largo plazo / Long-Term) integra el material de validación (la cadena de certificados y, opcionalmente, las respuestas OCSP o CRL) dentro del archivo, de modo que la firma aún pueda validarse años después cuando la infraestructura emisora ya no exista. B-LTA (Largo plazo con marca de tiempo de archivo) envuelve ese material en una marca de tiempo del documento, protegiendo los datos a largo plazo en sí y proporcionando un punto para volver a aplicar la marca de tiempo antes de que la criptografía subyacente quede obsoleta
La interpretación práctica se refiere al horizonte temporal. Una firma B-B responde a "¿alguien firmó esto?". La B-T responde a "¿y cuándo, de forma demostrable?". La B-LT responde a "¿y puedo seguir comprobándolo después de que los certificados expiren?". La B-LTA responde a "¿y esa comprobación seguirá siendo válida en veinte años?". Los perfiles regulatorios eligen un nivel: muchos entornos de facturación electrónica y eIDAS requieren al menos B-T, y los mandatos de archivo apuntan a B-LT o B-LTA. Conocer qué nivel alcanza realmente un documento, antes de aceptarlo o rechazarlo, es el propósito central del paso de inspección
Detectar el nivel base con TPdf.ValidatePades
El PDFium Component reduce toda la cuestión del nivel a una sola llamada. TPdf.ValidatePades devuelve un registro TPadesValidationResult cuyo campo Level es un TPadesLevel (plNone, plUnknown, plB_B, plB_T, plB_LT o plB_LTA), junto con un conjunto de problemas (issues), un conteo de firmas y un conteo de marcas de tiempo del documento. El nivel se infiere de forma progresiva: el validador establece B-B primero, luego lo asciende a B-T si hay una marca de tiempo de firma o una marca de tiempo de documento, a B-LT si el catálogo contiene una entrada /DSS con certificados y el marcador /Extensions /ESIC de Nivel 1, y a B-LTA si están presentes tanto una marca de tiempo de documento como el marcador de Nivel 2 de ESIC. Dos asistentes hacen que el resultado sea utilizable: IsCompliant es True solo cuando el nivel alcanza al menos B-B y el conjunto de problemas está vacío, e IsCompliantAt le permite establecer una base de política como plB_T
var
Pdf: TPdf;
R: TPadesValidationResult;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := 'contract-signed.pdf';
Pdf.Active := True;
R := Pdf.ValidatePades;
case R.Level of
plNone: Writeln('No PAdES signature present');
plUnknown: Writeln('Signature present but level undeterminable');
plB_B: Writeln('PAdES B-B (basic)');
plB_T: Writeln('PAdES B-T (trusted timestamp)');
plB_LT: Writeln('PAdES B-LT (long-term material embedded)');
plB_LTA: Writeln('PAdES B-LTA (archive timestamp)');
end;
Writeln('Signatures : ', R.SignatureCount);
Writeln('DocTimeStamps: ', R.DocTimeStampCount);
if R.IsCompliantAt(plB_T) then
Writeln('Meets the B-T policy floor')
else
Writeln('Below the required B-T level');
finally
Pdf.Free;
end;
end;
¿Por qué adbe.pkcs7.sha1 es un SubFilter prohibido?
Porque SHA-1 es inseguro y el controlador adbe.pkcs7.sha1 lo incluye directamente. Ese SubFilter calcula previamente el hash del documento con SHA-1 antes de envolverlo en PKCS#7, y SHA-1 ha sido vulnerable a colisiones durante años, por lo que la cláusula 6.3 de la norma EN 319 142-1 lo prohíbe rotundamente para una firma base. ValidatePades genera ppeiForbiddenSubFilter cuando encuentra adbe.pkcs7.sha1 o adbe.x509.rsa_sha1, y genera ppeiBadDigestAlgorithm cuando el CMS en sí utiliza MD5 o SHA-1 como resumen del mensaje (digest) (cláusula 6.2.1). Estas son dos comprobaciones distintas que capturan la misma clase de debilidad en dos capas diferentes
El conjunto de problemas tiene 26 miembros en total, y los que encontrará con más frecuencia se agrupan en torno a la estructura y la cobertura. ppeiByteRangeNotCoveringFile es la comprobación de cobertura descrita anteriormente. ppeiForbiddenCertKey se activa cuando el diccionario de la firma contiene una entrada /Cert, lo cual PAdES prohíbe porque la cadena debe residir en su lugar dentro del elemento SignedData.certificates del CMS. ppeiMissingSigningCertificate, ppeiMissingContentType y ppeiMissingMessageDigest marcan atributos firmados obligatorios que están ausentes, y ppeiDetachedContentViolation captura una firma que incrusta incorrectamente el contenido firmado en lugar de separarlo. Enumerar el conjunto convierte un simple rechazo en un diagnóstico que usted puede registrar en la bitácora
var
R: TPadesValidationResult;
Issue: TPadesValidationIssue;
begin
R := Pdf.ValidatePades;
if R.Issues <> [] then
for Issue := Low(TPadesValidationIssue) to High(TPadesValidationIssue) do
if Issue in R.Issues then
Writeln('Issue: ',
GetEnumName(TypeInfo(TPadesValidationIssue), Ord(Issue)));
end;
Lo que ValidatePades no comprueba
ValidatePades valida la estructura, no la confianza, y confundir una con la otra es un error peligroso. Un resultado de plB_LTA significa que el documento contiene una firma B-LTA bien formada con todos los atributos, materiales y marcas de tiempo obligatorios en los lugares correctos; no significa que la firma sea criptográficamente válida, que el certificado se conecte a una raíz en la que confía, o que ningún certificado de la cadena haya sido revocado. El validador deliberadamente no realiza ninguna verificación criptográfica: no vuelve a calcular la firma sobre el ByteRange, no construye ni evalúa la cadena de confianza y no comprueba el estado de revocación OCSP o CRL. Esta división es intencional y útil, porque la inspección estructural es rápida, completamente determinista y no requiere claves, red ni criptografía de la plataforma, por lo que ValidatePades se ejecuta de forma idéntica en Windows, Linux y macOS como Pascal puro sobre los bytes del archivo. La validación de la cadena de confianza, por el contrario, no se puede separar de la política (en qué raíces confía, cómo obtiene la revocación, qué tan estricta es su tolerancia a la marca de tiempo) y pertenece a una etapa posterior que depende del almacén de certificados de la plataforma; por lo tanto, trate un ValidatePades exitoso como el paso necesario de que una firma tiene la forma correcta, y luego entregue una firma estructuralmente sólida a la verificación criptográfica real antes de confiar en ella
Ese paso estructural es el lugar adecuado para comenzar, y se complementa de forma natural con las comprobaciones de solo lectura más amplias en auditar riesgos de seguridad de PDF con el PDFium Component y con tareas de conformidad de formato como validar documentos PDF/X listos para imprimir. Una vez que puede leer y clasificar una firma, el siguiente paso es producir una: el segundo artículo de esta serie cubre firmar PDFs con PAdES B-B, y el tercero amplía esto a marcas de tiempo confiables y firmas a largo plazo B-LT y B-LTA. La inspección de firma de solo lectura y el clasificador ValidatePades que se muestran aquí forman parte del PDFium Component para Delphi, C++Builder y Lazarus