O PDFium VCL agora pode completar uma cadeia de assinatura PDF e checar revocação pela rede no backend OpenSSL dele: quando o OnlineRetrieval está habilitado, o ConfigureSslCmsVerifier instala um verificador que baixa certificados intermediários faltantes das URLs caIssuers da AIA e CRLs dos pontos de distribuição de CRL, dentro de um orçamento fixo de tempo, requisições e bytes por chamada de verificação. Certificados baixados são apenas material de cadeia. A confiança continua vindo exclusivamente do store do sistema e das âncoras que você configurar
O vão que isso fecha aparece na primeira vez que você valida PDFs do mundo real num servidor Linux. Uma boa parcela dos signatários embute no CMS só o próprio certificado leaf deles, então o OpenSSL não alcança uma raiz, o TrustStatus volta inválido, e a revocação nunca roda porque a cadeia nunca se tornou confiável. Antes da v3.121.0 o backend OpenSSL descrito em verificar assinaturas PDF com OpenSSL no PDFium VCL era estritamente offline e o OnlineRetrieval não tinha efeito sobre ele. Uma coisa vale declarar de cara: o próprio motor PDFium não faz verificação de CMS nenhuma, então toda regra abaixo vive na camada PAdES do componente e no binding OpenSSL dele, onde você pode lê-la
Em que ordem o backend OpenSSL verifica, busca e checa?
Primeiro integridade, depois confiança, depois revocação, e a rede só é tocada entre os passos que precisam dela. O VerifyCmsWithSsl checa a assinatura CMS e os signed attributes (RFC 5652) com a avaliação de cadeia suprimida, e se isso falhar ele retorna imediatamente, antes mesmo de existir uma sessão de busca, então um documento com bytes quebrados não dispara requisição alguma para fora. Só se a cadeia então falhar e o OnlineRetrieval estiver ligado é que ele segue os links da AIA e verifica de novo. Os pontos de distribuição de CRL são buscados só quando a cadeia é confiável, porque uma CRL pendurada num caminho não confiável não prova nada. Os três vereditos ficam separados o tempo todo: uma assinatura válida com cadeia incompleta ainda é reportada como assinatura válida
uses
PDFium, FPdfCrypto, FPdfCryptoSsl, FPdfPades;
var
Pdf: TPdf;
Probe: TPdfCmsVerifyOptions;
Diags: TPdfSslVerifyDiagnostics;
Trust: TPadesTrustValidationOptions;
Verdict: TPadesValidationResult;
I: Integer;
begin
ConfigureSslTrustAnchors(LoadCorporateRoots); // DER; a única confiança 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 padrão
Trust.NetworkPolicy := ptnpOnline;
Trust.CheckRevocation := True;
Trust.UrlRetrievalTimeoutMs := 10000; // por chamada de verificação
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 que certificados baixados nunca entram no trust store?
Porque as URLs vêm do certificado sendo validado, e foi o signatário que as escolheu. A entrada caIssuers do authorityInfoAccess (RFC 5280 §4.2.2.1) é uma dica de onde o issuer mora, nada mais. Se o que quer que responda essa URL fosse para o store de âncoras de confiança, qualquer um poderia assinar com uma chave feita em casa, apontar a AIA para o próprio servidor e receber veredito verde. O RetrieveIntermediates portanto entrega cada certificado parseado ao CMS_add1_cert, que o coloca no conjunto untrusted dessa estrutura de CMS em particular, e o OpenSSL ainda precisa construir um caminho dele até uma âncora que você configurou ou que o store do sistema já tenha. Há uma razão mais silenciosa também: o argumento de certificados do CMS_verify não é um substituto pronto para os certificados embutidos no CMS, então adicionar ao próprio CMS é o caminho confiável
O loop de busca é deliberadamente estreito. O RetrieveIntermediates roda no máximo 4 rodadas, cada uma coletando URLs caIssuers de todo certificado agora no CMS, e para assim que uma rodada não adiciona nada ou o orçamento de tempo se esgota. Uma resposta precisa decodificar com o d2i_X509 como um único certificado DER que consome o corpo inteiro; bytes excedentes são rejeitados, e um bundle PKCS#7 só de certificados servido de uma URL .p7c é pulado em vez de desempacotado. O método de acesso OCSP na mesma extensão AIA é ignorado, já que esse backend não fala OCSP. Do lado da revocação, o RetrieveCrls lê só as URIs fullName de cada DistributionPoint (RFC 5280 §4.2.1.13) dos certificados do CMS e das âncoras configuradas, e as CRLs baixadas vão para um segundo X509_STORE independente com checagem de CRL de cadeia completa, então uma CRL ausente ou vencida muda o RevocationStatus sem nunca tocar o TrustStatus
// Condensado de VerifyCmsWithSsl (FPdfCryptoSsl.pas); configuração de BIO omitida.
// Toda chamada _CMS_verify recebe um content BIO fresco
if _CMS_verify(Cms, nil, nil, Bio, nil,
CMS_NO_SIGNER_CERT_VERIFY or CMS_BINARY) <> 1 then
Exit; // assinatura quebrada: rede nenhuma
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, só untrusted
// verifique a cadeia de novo contra o mesmo store de âncoras
end;
if Options.CheckRevocation and (FetchSession <> nil) and
(Result.TrustStatus = pcvsValid) then
RetrievedCrls := RetrieveCrls(Cms, FetchSession);
// segundo store, separado: CRLs configuradas mais as recuperadas
Store := BuildStore(True, RetrievedCrls, RevocationChecked, CrlsMalformed);
Quanto custa no máximo uma chamada de verificação?
Um teto fixo, imposto por um único TPdfCryptoFetchSession que os passos de AIA e CRL de uma única chamada de verificação compartilham. Os limites são constantes no FPdfCryptoHttp, não sugestões:
- Tempo: o
UrlRetrievalTimeoutMs, cujo default é 15000 tanto noTPdfCmsVerifyOptions.Defaultquanto noTPadesTrustValidationOptions.Default; uma sessão criada com 0 cai para 30000, e o relógio começa quando a assinatura passa, cobrindo toda requisição posterior - Requisições: no máximo 8 por sessão, contadas antes de o transporte ser tentado, então um host morto ainda consome um slot
- Bytes: 1 MiB por resposta e 4 MiB no total, com URLs de mais de 2048 caracteres recusadas antes de qualquer conexão
A contabilidade é mais estrita do que parece à primeira vista. Bytes recebidos de uma resposta com falha ainda contam no total, então um servidor respondendo 404 com uma página grande não drena o orçamento de graça. A leitura que cruza o limite por resposta aborta o download em vez de passar um corpo truncado ao parser ASN.1, e um HTTP 200 com corpo vazio é rejeitado de cara, porque o caminho da AIA caso contrário indexaria Data[0] de um array vazio. Só URLs http:// e https:// puras passam, sem redirects, cookies, credenciais ou descoberta automática de proxy, enquanto o HTTPS mantém suas checagens normais de certificado e hostname. A deduplicação de URLs tem escopo de uma chamada de propósito: a próxima validação precisa poder ver uma CRL recém-publicada. O orçamento também é por chamada, não por documento, e o ValidatePadesTrust verifica cada assinatura e cada token de timestamp separadamente, então o pior caso cresce com o número de assinaturas
Por que uma requisição WinHTTP que deu timeout ainda pode escrever na sua memória?
Porque retornar no timeout não cancela os callbacks já em voo. O transporte Windows comanda o WinHTTP de forma assíncrona e espera num evento pelo tempo restante da sessão, e quando essa espera desiste, a requisição ainda pode completar uma leitura e sinalizar depois. Aponte a leitura assíncrona para um buffer na stack e essa conclusão tardia escreve num frame que a essa altura pertence a uma função qualquer sem relação. O fix é propriedade, não timing: o evento e o buffer de leitura de 16 KB vivem num record no heap com duas referências, uma segurada pelo chamador e outra liberada só pelo callback final HANDLE_CLOSING, então o lado que terminar por último libera a memória
type
PHttpState = ^THttpState;
THttpState = record
References: LongInt; // chamador + callback final HANDLE_CLOSING
Event: THandle;
Status, Count: DWORD;
Buffer: array[0..16383] of Byte; // leituras assíncronas caem aqui, nunca numa stack
end;
procedure ReleaseState(State: PHttpState);
begin
if InterlockedDecrement(State.References) = 0 then
begin
CloseHandle(State.Event);
Dispose(State);
end;
end;
// No callback de status: HANDLE_CLOSING é a última notificação que o WinHTTP
// envia para a requisição, então ele solta a segunda referência
if Status = HttpHandleClosing then
begin
ReleaseState(State);
Exit;
end;
O que a libcurl precisa fornecer no FPC Unix
Um resolver assíncrono e um build thread-safe, ou a busca online fica desligada. No FPC Unix o transporte passa pela libcurl, a mesma dependência por trás do backend de timestamp libcurl para alvos não Windows, e o binding recusa qualquer biblioteca cuja máscara de features não tenha nem CURL_VERSION_ASYNCHDNS nem CURL_VERSION_THREADSAFE. A razão é que o CURLOPT_NOSIGNAL, que uma biblioteca dentro do processo de outra pessoa precisa setar, combinado com um resolver síncrono significa que um lookup de DNS pode simplesmente sobreviver ao timeout. A segunda armadilha é o shutdown: o curl_global_cleanup não espera pelas threads de DNS assíncronas, então uma vez que a libcurl foi inicializada o módulo fica mapeado até o processo sair, em vez de deixar uma thread em background correr para dentro de código descarregado. Quando um dos requisitos falha, o SslCapabilities.OnlineRetrieval é False e o SslVerifyOptionsDiagnostics reporta psvdOnlineRetrievalIgnored em vez de fingir que a rede foi consultada
O que o resultado garante e o que não garante
Um RevocationStatus válido desse backend significa que CRLs atuais cobrindo a cadeia inteira foram achadas, configuradas ou baixadas, e nenhuma listou um certificado dela; nada mais. Não há OCSP, então uma CA que publica revocação só por OCSP deixa o resultado como não suportado, e uma falha de rede parece exatamente com uma CA que não publica nada. Note também que o psvdNoCrlsConfigured descreve só as CRLs que você configurou, então com busca online ele é uma dica, não uma previsão de falha. Quando uma trilha de auditoria precisa ser reproduzível sem acesso à rede, deixe o NetworkPolicy no default ptnpOffline dele: nenhuma sessão de busca é criada e o backend nunca abre uma conexão, o que casa com o contrato offline do lado da CryptoAPI descrito em checagens de revocação offline de assinaturas PDF no Windows
O código de busca, os orçamentos e os bindings de transporte vão como source com o componente PDFium para Delphi, então você pode confirmar exatamente quais URLs uma validação pode contatar e quanto ela pode baixar antes de habilitar o ptnpOnline num servidor que manuseia documentos não confiáveis