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 hasta qué punto cumple con los requisitos a largo plazo. PDFium Component para Delphi y Lazarus responde a estas cuatro preguntas con llamadas de solo lectura: FPDF_GetSignatureCount y la familia FPDFSignatureObj_* exponen el diccionario de firma, y TPdf.ValidatePades clasifica el nivel de referencia 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 quedar claro desde el principio: todo lo que se describe aquí es inspección, y leer lo que afirma una firma es un trabajo diferente de 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 indican qué parte del archivo está protegida realmente. ISO 32000-1 §12.8 define el diccionario de firma con las entradas /ByteRange y /Contents. /Contents contiene una estructura CMS SignedData codificada en hexadecimal (RFC 5652), el sobre 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 subestiman: es un arreglo de dos intervalos de longitud y desplazamiento (offset) que juntos cubren todo el archivo excepto la cadena hexadecimal de /Contents. Ese espacio es exactamente donde residen los bytes de la firma, y los dos intervalos a cada lado son precisamente a lo que se compromete la firma
El diseño de ByteRange es lo que hace que un guardado incremental sea auditable. Debido a 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 de /Contents y todo lo demás se procesa en el hash de la firma. Una firma cuyo ByteRange no llega al final del archivo es una señal de advertencia: el contenido añadido después del rango cubierto, a través de 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 serio no es quién firmó, sino si la firma cubre los bytes que parece respaldar
Lectura del diccionario de firma con la API de solo lectura de PDFium
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 accesores de FPDFSignatureObj_* para /SubFilter, /ByteRange, /Contents, /Reason y la hora de firma. "Solo lectura" es la frase clave aquí: PDFium puede enumerar y leer firmas pero carece de una API para crear o escribir una, razón por la cual la parte de firma de esta serie está implementada por la propia biblioteca en lugar de por 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;
Cada registro TPdfSignature se asigna directamente al diccionario. Encoding representa al /SubFilter, el campo de diagnóstico más importante, porque indica el manejador de la firma y separa inmediatamente una firma moderna ETSI.CAdES.detached de una obsoleta o prohibida. Time es la hora de firma declarada por el autor como una cadena de fecha PDF, que representa una afirmación del firmante y no una hora de confianza. Content es la estructura CMS SignedData sin procesar, y Permission expone el nivel de certificación DocMDP (0 para una firma de aprobación ordinaria, y de 1 a 3 para una firma de certificación que bloquea cambios posteriores). El único campo que el registro no expone es el ByteRange analizado, y esa omisión es deliberada, porque ValidatePades realiza el cálculo de cobertura de ByteRange por usted en lugar de hacerle repetirlo a mano
¿Cuál es la diferencia entre PAdES B-B, B-T, B-LT y B-LTA?
Los cuatro niveles de referencia de PAdES forman una escala que va desde una firma mínimamente válida hasta una diseñada para sobrevivir a décadas de archivado, y cada nivel contiene estrictamente al inferior. ETSI EN 319 142-1 los define como B-B, B-T, B-LT y B-LTA. B-B (Básico) representa la firma más los atributos firmados obligatorios y nada más. B-T (Marca de tiempo) añade una marca de tiempo de confianza RFC 3161 sobre la firma, de modo que el momento de la firma está certificado por una autoridad de sellado de tiempo en lugar de afirmado por el reloj del firmante. B-LT (A largo plazo) incrusta el material de validación —la cadena de certificados y, opcionalmente, respuestas OCSP o CRL— dentro del archivo, para que la firma se pueda validar años después cuando la infraestructura emisora ya no exista. B-LTA (A largo plazo con marca de tiempo de archivo) envuelve una marca de tiempo del documento alrededor de ese material, protegiendo los datos a largo plazo y dándole un punto para volver a aplicar marcas de tiempo antes de que la criptografía subyacente caduque
La interpretación práctica se refiere al horizonte temporal. Una firma B-B responde a "¿alguien firmó esto?". B-T responde a "y cuándo, de forma demostrable". B-LT responde a "y si todavía puedo verificarla después de que los certificados expiren". B-LTA responde a "y si esa comprobación seguirá siendo válida dentro de veinte años". Los perfiles regulatorios eligen un escalón: muchos contextos 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é escalón alcanza realmente un documento, antes de aceptarlo o rechazarlo, es el objetivo principal del paso de inspección
Detección del nivel de referencia con TPdf.ValidatePades
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 incidencias, un recuento de firmas y un recuento de marcas de tiempo del documento. El nivel se infiere de forma monótona: el validador establece B-B primero, luego asciende a B-T si hay una marca de tiempo de firma o de documento presente, a B-LT si el catálogo contiene un /DSS con certificados y el marcador /Extensions /ESIC de Nivel 1, y a B-LTA si están presentes tanto una marca de tiempo del documento como el marcador ESIC Nivel 2. Dos asistentes hacen que el resultado sea procesable: IsCompliant es True solo cuando el nivel alcanza al menos B-B y el conjunto de incidencias está vacío, e IsCompliantAt le permite exigir un nivel mínimo 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 no es seguro y el manejador adbe.pkcs7.sha1 lo incluye de forma fija. Ese SubFilter precalcula el hash del documento con SHA-1 antes de envolverlo en PKCS#7, y SHA-1 es vulnerable a colisiones desde hace años, por lo que la cláusula 6.3 de EN 319 142-1 lo prohíbe rotundamente para una firma de referencia. ValidatePades genera ppeiForbiddenSubFilter cuando detecta adbe.pkcs7.sha1 o adbe.x509.rsa_sha1, y genera ppeiBadDigestAlgorithm cuando el propio CMS utiliza MD5 o SHA-1 como resumen de mensaje (cláusula 6.2.1). Estas son dos comprobaciones distintas que detectan la misma clase de debilidad en dos capas diferentes
El conjunto de incidencias tiene 26 miembros en total, y los que encontrará con más frecuencia se concentran en torno a la estructura y la cobertura. ppeiByteRangeNotCoveringFile representa la comprobación de cobertura descrita anteriormente. ppeiForbiddenCertKey se activa cuando el diccionario de firma contiene una entrada /Cert, lo que PAdES prohíbe porque la cadena debe residir dentro de SignedData.certificates del CMS en su lugar. ppeiMissingSigningCertificate, ppeiMissingContentType y ppeiMissingMessageDigest señalan atributos firmados obligatorios que están ausentes, y ppeiDetachedContentViolation detecta una firma que incrusta erróneamente el contenido firmado en lugar de mantenerlo separado (detached). Enumerar el conjunto convierte un simple rechazo en un diagnóstico que 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 otra representa el 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 adecuados —no significa que la firma sea criptográficamente válida, que el certificado se conecte con 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 separación es intencionada y útil, porque la inspección estructural es rápida, completamente determinista y no requiere claves, red ni criptografía de 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, es inseparable 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 que debe tratar a un ValidatePades exitoso como el filtro necesario de que una firma tiene la estructura correcta, y luego entregar 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 combina naturalmente con las comprobaciones de solo lectura más amplias en auditoría de riesgos de seguridad de PDF con PDFium Component y con tareas de conformidad de formato como la validación de documentos PDF/X listos para imprimir. Una vez que pueda leer y clasificar una firma, el siguiente paso es producirla: el segundo artículo de esta serie cubre la firma de archivos PDF con PAdES B-B, y el tercero amplía esto a marcas de tiempo de confianza 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 de PDFium Component para Delphi, C++Builder y Lazarus