PDFium VCL ahora puede completar una cadena de firma PDF y chequear revocación por la red en su backend OpenSSL: cuando OnlineRetrieval está habilitado, ConfigureSslCmsVerifier instala un verificador que descarga los certificados intermedios faltantes desde las URLs AIA caIssuers y las CRLs desde los CRL distribution points, dentro de un presupuesto fijo de tiempo, requests y bytes por llamada de verificación. Los certificados descargados son material de cadena y nada más. La confianza sigue viniendo exclusivamente del store del sistema y de las anclas que usted configure
La brecha que esto cierra aparece la primera vez que valida PDFs reales en un servidor Linux. Una buena parte de los firmantes incrusta en el CMS solo su propio certificado hoja, así que OpenSSL no puede llegar a una raíz, TrustStatus vuelve inválido, y la revocación nunca corre porque la cadena nunca llegó a ser confiable. Antes de la v3.121.0 el backend OpenSSL descrito en verificar firmas PDF con OpenSSL en PDFium VCL era estrictamente offline y OnlineRetrieval no tenía efecto sobre él. Una cosa vale declararla de entrada: el motor PDFium en sí no hace verificación CMS alguna, así que cada regla de abajo vive en la capa PAdES del componente y en su binding OpenSSL, donde usted puede leerla
¿En qué orden verifica, descarga y chequea el backend OpenSSL?
Primero integridad, después confianza, después revocación, y la red se toca solo entre los pasos que la necesitan. VerifyCmsWithSsl chequea la firma CMS y los signed attributes (RFC 5652) con la evaluación de cadena suprimida, y si eso falla vuelve de inmediato, antes de que exista siquiera una sesión de descarga, así que un documento con bytes rotos no dispara ningún request de salida. Solo si la cadena entonces falla y OnlineRetrieval está activo sigue los enlaces AIA y verifica otra vez. Los CRL distribution points se descargan únicamente cuando la cadena ya es confiable, porque una CRL colgada de un camino no confiable no prueba nada. Los tres veredictos se mantienen separados de punta a punta: una firma válida con cadena incompleta sigue reportándose como firma válida
uses
PDFium, FPdfCrypto, FPdfCryptoSsl, FPdfPades;
var
Pdf: TPdf;
Probe: TPdfCmsVerifyOptions;
Diags: TPdfSslVerifyDiagnostics;
Trust: TPadesTrustValidationOptions;
Verdict: TPadesValidationResult;
I: Integer;
begin
ConfigureSslTrustAnchors(LoadCorporateRoots); // DER; la única confianza extra
ConfigureSslCmsVerifier;
Probe := TPdfCmsVerifyOptions.Default;
Probe.OnlineRetrieval := True;
Probe.CheckRevocation := True;
Diags := SslVerifyOptionsDiagnostics(Probe);
if psvdOnlineRetrievalIgnored in Diags then
Log('no HTTP transport or CMS_add1_cert: validation stays offline');
Trust := TPadesTrustValidationOptions.Default; // ptnpOffline por defecto
Trust.NetworkPolicy := ptnpOnline;
Trust.CheckRevocation := True;
Trust.UrlRetrievalTimeoutMs := 10000; // por llamada de verificación
Pdf := TPdf.Create(nil);
try
Pdf.FileName := 'signed-contract.pdf';
Pdf.Active := True;
Verdict := Pdf.ValidatePadesTrust(Trust);
for I := 0 to High(Verdict.Signatures) do
Log(Format('#%d trust=%d revocation=%d', [I,
Ord(Verdict.Signatures[I].CertificateTrustStatus),
Ord(Verdict.Signatures[I].RevocationStatus)]));
finally
Pdf.Free;
end;
end;
¿Por qué los certificados descargados jamás entran al trust store?
Porque las URLs vienen del certificado que se está validando, y quien las eligió fue el firmante. La entrada authorityInfoAccess caIssuers (RFC 5280 §4.2.2.1) es una pista sobre dónde vive el emisor, nada más. Si lo que responda a esa URL entrara al store de trust anchors, cualquiera podría firmar con una llave hecha en casa, apuntar el AIA a su propio servidor y recibir un veredicto verde. RetrieveIntermediates por eso le pasa cada certificado parseado a CMS_add1_cert, que lo coloca en el set no confiable de esta estructura CMS puntual, y OpenSSL todavía tiene que construir un camino desde él hasta una ancla que usted configuró o que el store del sistema ya tenga. Hay una razón más silenciosa también: el argumento de certificados de CMS_verify no es un sustituto directo de los certificados incrustados en el CMS, así que agregar al propio CMS es el camino confiable
El bucle de recuperación es deliberadamente angosto. RetrieveIntermediates corre a lo más 4 rondas, cada una colectando las URLs caIssuers de todo certificado que ya esté en el CMS, y para apenas una ronda no agrega nada o el presupuesto de tiempo se agota. Una respuesta debe decodificar con d2i_X509 como un único certificado DER que consuma el cuerpo entero; los bytes de sobra se rechazan, y un bundle PKCS#7 solo-certificados servido desde una URL .p7c se salta en lugar de desempacarse. El método de acceso OCSP de la misma extensión AIA se ignora, porque este backend no habla OCSP. Del lado de la revocación, RetrieveCrls lee solo las URIs fullName de cada DistributionPoint (RFC 5280 §4.2.1.13) de los certificados del CMS y de las anclas configuradas, y las CRLs descargadas van a un segundo X509_STORE independiente con chequeo de CRL de cadena completa, así que una CRL faltante o vencida cambia el RevocationStatus sin tocar jamás el TrustStatus
// Condensado de VerifyCmsWithSsl (FPdfCryptoSsl.pas); setup de BIO omitido.
// Cada llamada a _CMS_verify recibe un content BIO fresco
if _CMS_verify(Cms, nil, nil, Bio, nil,
CMS_NO_SIGNER_CERT_VERIFY or CMS_BINARY) <> 1 then
Exit; // firma rota: nada de red
if Options.OnlineRetrieval and SslCapabilities.OnlineRetrieval then
FetchSession := TPdfCryptoFetchSession.Create(Options.UrlRetrievalTimeoutMs);
Store := BuildStore(False, nil, RevocationChecked, CrlsMalformed);
if (_CMS_verify(Cms, nil, Store, Bio, nil, CMS_BINARY) <> 1) and
(FetchSession <> nil) then
begin
RetrieveIntermediates(Cms, FetchSession); // CMS_add1_cert, solo no confiable
// verificar la cadena otra vez contra el mismo store de anclas
end;
if Options.CheckRevocation and (FetchSession <> nil) and
(Result.TrustStatus = pcvsValid) then
RetrievedCrls := RetrieveCrls(Cms, FetchSession);
// segundo store, separado: CRLs configuradas más las recuperadas
Store := BuildStore(True, RetrievedCrls, RevocationChecked, CrlsMalformed);
¿Cuánto cuesta como máximo una llamada de verificación?
Un techo fijo, impuesto por un solo TPdfCryptoFetchSession que los pasos AIA y CRL de una misma llamada de verificación comparten. Los límites son constantes en FPdfCryptoHttp, no sugerencias:
- Tiempo:
UrlRetrievalTimeoutMs, que por defecto es 15000 tanto enTPdfCmsVerifyOptions.Defaultcomo enTPadesTrustValidationOptions.Default; una sesión creada con 0 cae a 30000, y el reloj arranca una vez que la firma pasó, cubriendo todo request posterior - Requests: a lo más 8 por sesión, contados antes de intentar el transporte, así que un host muerto igual consume un cupo
- Bytes: 1 MiB por respuesta y 4 MiB en total, con URLs de más de 2048 caracteres rechazadas antes de cualquier conexión
La contabilidad es más estricta de lo que parece a primera vista. Los bytes recibidos de una respuesta fallida cuentan contra el total, así que un servidor que responde 404 con una página grande no puede drenar el presupuesto gratis. La lectura que cruza el límite por respuesta aborta la descarga en lugar de entregarle un cuerpo truncado al parser ASN.1, y un HTTP 200 con cuerpo vacío se rechaza de plano, porque el camino AIA indexaría Data[0] de un array vacío. Solo pasan las URLs http:// y https:// peladas, sin redirects, cookies, credenciales ni descubrimiento automático de proxy, mientras HTTPS mantiene sus chequeos normales de certificado y hostname. La deduplicación de URLs queda acotada a una llamada a propósito: la siguiente validación debe poder ver una CRL recién publicada. El presupuesto además es por llamada, no por documento, y ValidatePadesTrust verifica cada firma y cada token de timestamp por separado, así que el peor caso crece con el número de firmas
¿Por qué un request WinHTTP con timeout aún puede escribir en su memoria?
Porque volver por timeout no cancela los callbacks que ya están en vuelo. El transporte Windows maneja WinHTTP en forma asíncrona y espera sobre un evento con el tiempo restante de la sesión, y cuando esa espera se rinde, el request todavía puede completar una lectura y señalizar después. Apunte la lectura asíncrona a un buffer del stack y esa finalización tardía escribe en un frame que para entonces pertenece a alguna función ajena. El fix es propiedad, no timing: el evento y el buffer de lectura de 16 KB viven en un record del heap con dos referencias, una del llamador y otra que solo libera el callback final HANDLE_CLOSING, así que el lado que termine último libera la memoria
type
PHttpState = ^THttpState;
THttpState = record
References: LongInt; // llamador + callback final HANDLE_CLOSING
Event: THandle;
Status, Count: DWORD;
Buffer: array[0..16383] of Byte; // las lecturas async aterrizan aquí, nunca en el stack
end;
procedure ReleaseState(State: PHttpState);
begin
if InterlockedDecrement(State.References) = 0 then
begin
CloseHandle(State.Event);
Dispose(State);
end;
end;
// En el callback de estado: HANDLE_CLOSING es la última notificación que
// WinHTTP envía del request, así que suelta la segunda referencia
if Status = HttpHandleClosing then
begin
ReleaseState(State);
Exit;
end;
Qué debe proveer libcurl en FPC Unix
Un resolver asíncrono y un build thread-safe, o la recuperación online queda apagada. En FPC Unix el transporte va por libcurl, la misma dependencia detrás del backend de timestamps libcurl para targets no Windows, y el binding rechaza cualquier librería cuya máscara de features carezca de CURL_VERSION_ASYNCHDNS o de CURL_VERSION_THREADSAFE. La razón es que CURLOPT_NOSIGNAL, que una librería dentro del proceso de otro debe fijar, combinado con un resolver síncrono significa que una lookup DNS puede simplemente sobrevivir al timeout. La segunda trampa es el apagado: curl_global_cleanup no espera a los threads DNS asíncronos, así que una vez que libcurl se inicializó el módulo queda mapeado hasta que el proceso termina, en lugar de dejar que un thread de fondo corra dentro de código descargado. Cuando cualquiera de los dos requisitos falla, SslCapabilities.OnlineRetrieval es False y SslVerifyOptionsDiagnostics reporta psvdOnlineRetrievalIgnored en lugar de fingir que se consultó la red
Lo que el resultado garantiza y lo que no
Un RevocationStatus válido de este backend significa que se encontraron, configuraron o descargaron CRLs vigentes cubriendo toda la cadena, y que ninguna listó un certificado de ella; nada más. No hay OCSP, así que una CA que publica revocación solo por OCSP deja el resultado como no soportado, y una falla de red se ve exactamente igual que una CA que no publica nada. Note además que psvdNoCrlsConfigured describe solo las CRLs que usted configuró, así que con recuperación online es una pista, no un pronóstico de falla. Cuando una pista de auditoría debe ser reproducible sin acceso a red, deje NetworkPolicy en su default ptnpOffline: no se crea ninguna sesión de descarga y el backend jamás abre una conexión, lo que cuadra con el contrato offline del lado CryptoAPI descrito en chequeos de revocación de firmas PDF offline en Windows
El código de recuperación, los presupuestos y los bindings de transporte vienen como fuente con el PDFium Delphi component, así que puede confirmar exactamente qué URLs puede contactar una validación y cuánto puede descargar antes de habilitar ptnpOnline en un servidor que procesa documentos no confiables