PDFium VCL trata la verificación CMS como un backend reemplazable detrás de la interfaz IPdfCmsVerifier, de modo que el validador PAdES puede correr en Windows a través de CryptoAPI, en macOS a través del Keychain, y en cualquier sitio donde haya OpenSSL mediante ConfigureSslCmsVerifier. La interfaz es pequeña. Tres comportamientos de OpenSSL por debajo de ella producen respuestas seguras y equivocadas si usted la implementa de forma ingenua
La motivación es bastante clara en cuanto una aplicación Delphi sale de Windows. La validación de firmas es una de las pocas áreas donde el stack criptográfico de la plataforma no es un detalle de implementación: decide qué certificados son de confianza, qué algoritmos existen y qué significa la revocación. Atornille uno al código y ese código no se porta. Abstráigalo mal y cada plataforma reporta una respuesta con una forma distinta que el llamador no puede comparar
Lo que la abstracción tiene que cargar de verdad
Dos formas de verificación y tres veredictos independientes. Una firma PDF es detached: el contenido firmado son los dos rangos de bytes a cada lado del hueco de /Contents, así que VerifyDetached toma dos segmentos en lugar de un buffer. Un token de marca de tiempo es attached, lleva su propio contenido, así que VerifyAttached toma solo el DER
El resultado se divide en tres estados porque responden a tres preguntas distintas y pueden discrepar. SignatureStatus dice si los bytes fueron firmados por la clave del certificado del firmante. TrustStatus dice si ese certificado encadena a algo en lo que usted confía. RevocationStatus dice si el certificado seguía siendo válido en el momento pertinente. Un documento con una firma matemáticamente perfecta de un certificado del que usted nunca ha oído hablar es válido, no es de confianza y es desconocido, y aplastar eso en un solo booleano es como los validadores acaban mintiendo a los usuarios
uses
FPdfCrypto, FPdfCryptoSsl;
var
Options: TPdfCmsVerifyOptions;
begin
if not SslAvailable then
raise Exception.Create('libcrypto not usable: ' + SslMissingSymbols);
ConfigureSslTrustAnchors(LoadCorporateRoots); // DER, puede estar vacío
ConfigureSslCrls(LoadFreshCrls); // DER, puede estar vacío
ConfigureSslCmsVerifier; // instala el backend
Writeln('backend : ', PadesCmsVerificationBackendName);
Writeln('library : ', SslLibraryPath, ' ', SslLibraryVersion);
Writeln('ABI : ', SslAbiLayout); // ulong=<n> long=<n>
Options := TPdfCmsVerifyOptions.Default;
Options.CheckRevocation := True;
Options.CollectChainCertificates := True;
end;
SslAbiLayout parece una curiosidad y no lo es. Cada código de error de OpenSSL y cada flag del store cruza la frontera como un unsigned long de C, que son cuatro bytes en Windows y ocho en Linux y macOS. Declárelo como un tipo fijo de 32 bits y el código funciona en Windows, y después lee en silencio medio valor en LP64. Reportar los anchos supuestos como una cadena sobre la que puede hacer assert en un test convierte toda una clase de deriva de ABI de plataforma en una comprobación de una línea. Cualquiera que haya peleado con el mismo problema con CK_ULONG en un binding PKCS#11 lo reconocerá de inmediato; esa historia está en el empaquetado de structs PKCS#11 y el ancho de CK_ULONG
¿Por qué la segunda pasada de verificación ve contenido vacío?
Porque CMS_verify lee el BIO del contenido detached hasta el fin de fichero, y un BIO que ya se leyó no se rebobina solo. Verificar en dos pasadas es un diseño razonable, primero la firma criptográfica sola con la evaluación de cadena suprimida, después la evaluación completa, y falla de una forma inusualmente engañosa si ambas pasadas comparten un BIO
La segunda pasada recibe cero bytes de contenido. En modo detached eso no es un error, porque un buffer de contenido vacío es una entrada legal. El digest simplemente no coincide, y el fallo se manifiesta como un fallo de construcción de cadena y no como un fallo de contenido, lo que le manda a inspeccionar certificados y almacenes de confianza mientras el problema real es una posición de stream. Reconstruya el BIO de memoria con BIO_new_mem_buf en cada pasada. Cuesta una asignación y elimina la posibilidad por completo
Lo que el flag no-verify suprime y lo que no
CMS_NO_SIGNER_CERT_VERIFY suprime la evaluación de cadena, no la búsqueda del certificado del firmante. Internamente OpenSSL resuelve y adjunta los certificados del firmante antes de consultar el flag, así que después de una primera pasada que lleva ese flag el firmante ya está disponible y sus identificadores de algoritmo se pueden leer directamente. No hace falta correr una segunda verificación completa solo para obtener el certificado del firmante, que es justo lo que el nombre del flag le tienta a suponer
Una regla de propiedad va con eso. La referencia al firmante pertenece a la estructura CMS y no debe liberarse por su cuenta. Es válida mientras la estructura lo sea, y liberarla produce una corrupción cuyo síntoma aparece en un sitio totalmente distinto, normalmente durante la limpieza de un objeto sin relación
¿Por qué activar la comprobación de CRL rechaza todas las firmas?
Porque OpenSSL comprueba las CRL solo contra lo que el store ya contiene y no descarga nada por su cuenta. No sigue los CRL distribution points y no habla OCSP. Active X509_V_FLAG_CRL_CHECK en un store sin CRLs y todas las cadenas fallan con una incapacidad de obtener la CRL del certificado. El resultado parece una comprobación de revocación que funciona y encuentra problemas. Es una comprobación de revocación que nunca llega a correr
El backend por eso activa el flag solo cuando ConfigureSslCrls ha suministrado de verdad al menos una CRL. Sin ninguna, RevocationStatus vuelve como pcvsUnsupported, que es una declaración honesta de que la pregunta no quedó respondida. Por la misma razón OnlineRetrieval no tiene efecto en este backend y no se emite ningún checkpoint pcvstOnlineRetrieval: no hay ruta de descarga desde la que reportar progreso
Esta es una postura de diseño que conviene defender en general. Un validador que no puede comprobar la revocación debería decirlo. Reportar un certificado sin comprobar como no revocado es la manera más común de que las herramientas de validación de firmas despistan a sus usuarios, y es exactamente la clase de confusión que se explora en por qué los validadores rechazan firmas PAdES
// Los checkpoints dejan a una UI mostrar qué etapa corre, y le dicen
// qué etapas realiza de verdad un backend
type
TSignatureProbe = class
procedure Checkpoint(Stage: TPdfCmsVerifyStage);
end;
procedure TSignatureProbe.Checkpoint(Stage: TPdfCmsVerifyStage);
begin
case Stage of
pcvstCryptographicSignature: Status('checking the signature');
pcvstChainBuild: Status('building the certificate chain');
pcvstOnlineRetrieval: Status('fetching validation data');
pcvstRevocationCheck: Status('checking revocation');
end;
end;
// Lea los tres veredictos por separado; se les permite discrepar
if Result.SignatureStatus = pcvsValid then
case Result.TrustStatus of
pcvsValid: Report('signed and trusted');
pcvsInvalid: Report('signed, chain rejected');
pcvsUnsupported,
pcvsIndeterminate: Report('signed, trust not established');
end;
if Result.RevocationStatus = pcvsUnsupported then
Report('revocation was not checked on this backend');
Enlazarse a una biblioteca cuya versión no puede fijar
OpenSSL renombró sus accesores del stack entre 1.0 y 1.1, así que la misma función lógica tiene dos posibles nombres de exportación según la compilación que la máquina anfitriona tenga. El binding resuelve primero el nombre nuevo y recurre al viejo, y solo registra un símbolo ausente cuando no resuelve ninguno. Esa es la forma correcta de cualquier enlace dinámico contra una biblioteca que usted no distribuye: preferir los nombres actuales, tolerar los históricos y reportar solo la ausencia genuina
SslMissingSymbols es lo que convierte una carga fallida en un evento diagnosticable. Un resultado no vacío en una máquina que claramente tiene libcrypto instalada significa que la versión instalada es más vieja que la API a la que apunta esta compilación, que es una conversación de soporte completamente distinta a la de una biblioteca que falta. ConfigureSslLibraryPath cubre el otro caso común, una máquina con varias compilaciones de OpenSSL donde la que está en la ruta de búsqueda por defecto no es la que usted quiere
Elegir un backend por plataforma
El arreglo práctico es seleccionar en el arranque y registrar cuál respondió. En Windows, el backend de plataforma se integra con los almacenes de certificados que una empresa ya gestiona, que es normalmente lo que usted quiere. En macOS el backend de Keychain encaja con el mismo razonamiento y está descrito en verificar firmas con SecTrust en macOS. OpenSSL es la opción portable, y también es la elección correcta cuando necesita una política de validación idéntica entre plataformas en lugar de una que siga el almacén de confianza de cada plataforma
Instale el que instale, registre PadesCmsVerificationBackendName junto a cada veredicto que anote. Un resultado de validación guardado sin el backend que lo produjo no se puede reproducir después, porque los tres valores de estado significan cosas sutilmente distintas según qué stack respondió. La capa de inspección de firmas por encima de todo esto, incluido cómo se reportan los niveles PAdES, está cubierta en inspeccionar firmas digitales PDF y niveles PAdES
Todo ello se envía como fuente con el componente PDFium para Delphi, lo que aquí importa más que de costumbre: para un validador de firmas, poder leer exactamente qué flags activa un backend y qué comprobaciones se salta no es un lujo, es la única manera de saber qué afirma de verdad un check verde en su aplicación