PDFium VCL artık OpenSSL arka ucunda bir PDF imza zincirini tamamlayabilir ve iptali ağ üzerinden denetleyebilir: OnlineRetrieval etkinleştirildiğinde ConfigureSslCmsVerifier, AIA caIssuers URL'lerinden eksik ara sertifikaları ile CRL dağıtım noktalarından CRL'leri indiren bir doğrulayıcı kurar; her doğrulama çağrısı için sabit bir zaman, istek ve bayt bütçesi içinde. İndirilen sertifikalar hiçbir zaman yalnızca zincir malzemesidir. Güven hâlâ dışlayıcı olarak sistem deposundan ve sizin yapılandırdığınız çapalardan gelir
Kapatılan boşluk, gerçek dünya PDF'lerini bir Linux sunucusunda ilk kez doğruladığınızda ortaya çıkar. İmzalayanların büyük bölümü CMS'e yalnızca kendi yaprak sertifikasını gömer; OpenSSL köke ulaşamaz, TrustStatus geçersiz döner ve zincir güvenilir hale gelmediği için iptal hiç çalışmaz. v3.121.0 öncesinde PDFium VCL'de OpenSSL ile PDF imzalarını doğrulama makalesinde anlatılan OpenSSL arka ucu kesinlikle çevrimdışıydı ve OnlineRetrieval'ın üzerinde etkisi yoktu. Baştan söylemeye değer bir şey: PDFium motorunun kendisi hiç CMS doğrulaması yapmaz; aşağıdaki her kural bileşenin PAdES katmanında ve okuyabileceğiniz OpenSSL bağlamasında yaşar
OpenSSL arka ucu hangi sırayla doğrular, getirir ve denetler?
Önce bütünlük, sonra güven, sonra iptal; ağa yalnızca gerektiren adımların arasında dokunulur. VerifyCmsWithSsl CMS imzasını ve imzalı öznitelikleri (RFC 5652) zincir değerlendirmesi bastırılmış olarak denetler ve bu başarısız olursa, bir getiri oturumu daha var olmadan hemen döner; böylece baytları bozuk bir belge giden hiçbir istek tetiklemez. Yalnızca zincir sonra başarısız olursa ve OnlineRetrieval açıksa AIA bağlantılarını izler ve yeniden doğrular. CRL dağıtım noktaları, zincir güvenilir hale geldikten sonra getirilir; çünkü güvenilmeyen bir yola asılı bir CRL hiçbir şey kanıtlamaz. Üç karar da boyunca ayrı kalır: eksik zincirli geçerli bir imza hâlâ geçerli imza olarak bildirilir
uses
PDFium, FPdfCrypto, FPdfCryptoSsl, FPdfPades;
var
Pdf: TPdf;
Probe: TPdfCmsVerifyOptions;
Diags: TPdfSslVerifyDiagnostics;
Trust: TPadesTrustValidationOptions;
Verdict: TPadesValidationResult;
I: Integer;
begin
ConfigureSslTrustAnchors(LoadCorporateRoots); // DER; tek ekstra güven
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; // varsayılan ptnpOffline
Trust.NetworkPolicy := ptnpOnline;
Trust.CheckRevocation := True;
Trust.UrlRetrievalTimeoutMs := 10000; // doğrulama çağrısı başına
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;
İndirilen sertifikalar neden asla güven deposuna eklenmez?
Çünkü URL'ler doğrulanan sertifikadan gelir ve onları imzalayan seçmiştir. authorityInfoAccess caIssuers girdisi (RFC 5280 §4.2.2.1), verenin nerede yaşadığına dair bir ipucudur, daha fazlası değil. O URL'yi yanıtlayan her neyse güven çapası deposuna girseydi, herkes kendi yaptığı bir anahtarla imzalar, AIA'yı kendi sunucusuna gösterir ve yeşil bir karar alırdı. RetrieveIntermediates bu yüzden her ayrıştırılmış sertifikayı CMS_add1_cert'e verir; bu, onu bu tek CMS yapısının güvenilmeyen kümesine yerleştirir ve OpenSSL'in yine de ondan, sizin yapılandırdığınız ya da sistem deposunun zaten tuttuğu bir çapaya yol kurması gerekir. Daha sessiz bir neden de var: CMS_verify'in sertifika argümanı, CMS'e gömülü sertifikaların yerine geçen hazır bir çözüm değildir; dolayısıyla CMS'in kendisine eklemek güvenilir yoldur
Getiri döngüsü bilinçli olarak dardır. RetrieveIntermediates en çok 4 tur koşar, her tur CMS'te şu an bulunan her sertifikadan caIssuers URL'lerini toplar ve bir tur hiçbir şey eklemediğinde ya da zaman bütçesi harcandığında durur. Bir yanıtın d2i_X509 ile tüm gövdeyi tüketen tek bir DER sertifikası olarak çözülmesi gerekir; sondaki baytlar reddedilir ve bir .p7c URL'sinden sunulan PKCS#7 yalnızca-sertifika paketi açılmak yerine atlanır. Aynı AIA uzantısındaki OCSP erişim yöntemi yok sayılır; bu arka uç OCSP konuşmaz. İptal tarafında RetrieveCrls, CMS sertifikalarından ve yapılandırılmış çapalardan her DistributionPoint'in (RFC 5280 §4.2.1.13) yalnızca fullName URI'lerini okur ve indirilen CRL'ler tam zincirli CRL denetimiyle ikinci, bağımsız bir X509_STORE'a gider; böylece eksik ya da bayat bir CRL, TrustStatus'a hiç dokunmadan RevocationStatus'u değiştirir
// VerifyCmsWithSsl'den sıkıştırıldı (FPdfCryptoSsl.pas); BIO kurulumu atlandı.
// Her _CMS_verify çağrısı taze bir content BIO alır
if _CMS_verify(Cms, nil, nil, Bio, nil,
CMS_NO_SIGNER_CERT_VERIFY or CMS_BINARY) <> 1 then
Exit; // bozuk imza: hiç ağ yok
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, yalnızca güvenilmeyen
// zinciri aynı anchor deposuna karşı yeniden doğrula
end;
if Options.CheckRevocation and (FetchSession <> nil) and
(Result.TrustStatus = pcvsValid) then
RetrievedCrls := RetrieveCrls(Cms, FetchSession);
// ikinci, ayrı depo: yapılandırılmış CRL'ler artı indirilenler
Store := BuildStore(True, RetrievedCrls, RevocationChecked, CrlsMalformed);
Bir doğrulama çağrısı en çok neye mal olur?
Sabit bir tavan; tek bir doğrulama çağrısının AIA ve CRL adımlarının paylaştığı tek bir TPdfCryptoFetchSession tarafından zorlanır. Sınırlar FPdfCryptoHttp içinde sabittir, öneri değil:
- Zaman:
UrlRetrievalTimeoutMs; hemTPdfCmsVerifyOptions.DefaulthemTPadesTrustValidationOptions.Defaultiçinde 15000'dir; 0 ile kurulan bir oturum 30000'e döner ve saat, imza geçtikten sonra işlemeye başlar ve sonraki her isteği kapsar - İstekler: oturum başına en çok 8, taşıma denenmeden önce sayılır; böylece ölü bir ana makine de bir kotayı harcar
- Baytlar: yanıt başına 1 MiB, toplamda 4 MiB; 2048 karakterden uzun URL'ler hiç bağlantı kurulmadan reddedilir
Muhasebe ilk bakışta göründüğünden katıdır. Başarısız bir yanıttan alınan baytlar da toplama işler; böylece büyük bir sayfayla 404 yanıtlayan sunucu bütçeyi bedavaya tüketemez. Yanıt başına sınırı aşan okuma, kesilmiş gövdeyi ASN.1 ayrıştırıcısına devretmek yerine indirmeyi durdurur ve boş gövdeli bir HTTP 200 tümüyle reddedilir; çünkü AIA yolu aksi hâlde boş bir dizinin Data[0]'ına indekslerdi. Yalnızca düz http:// ve https:// URL'leri geçer; yönlendirme, çerez, kimlik bilgisi ya da otomatik vekil keşfi yoktur; HTTPS normal sertifika ve ana makine adı denetimlerini korur. URL tekilleştirme bilinçli olarak tek bir çağrıyla sınırlıdır: sonraki doğrulama, yeni yayımlanmış bir CRL'yi görebilmelidir. Bütçe ayrıca belge başına değil çağrı başınadır ve ValidatePadesTrust her imzayı ve her zaman damgası token'ını ayrı doğrular; en kötü durum imza sayısıyla büyür
Zaman aşımına uğramış bir WinHTTP isteği belleğinize hâlâ nasıl yazabilir?
Çünkü zaman aşımında dönmek, havada kalan callback'leri iptal etmez. Windows taşıması WinHTTP'yi eşzamansız sürer ve oturumun kalan zamanıyla bir olay üzerinde bekler; o bekleyiş vazgeçtiğinde istek yine de bir okumayı tamamlayıp sonra sinyal verebilir. Eşzamansız okumayı bir stack tamponuna yönlendirin, o geç tamamlanma o sırada alakasız bir fonksiyona ait olan çerçeveye yazar. Düzeltme zamanlama değil sahipliktir: olay ile 16 KB'lik okuma tamponu iki referanslı bir yığın kaydında yaşar; biri çağıranın, biri yalnızca son HANDLE_CLOSING callback'inin bıraktığı; hangisi son bitirirse belleği o boşaltır
type
PHttpState = ^THttpState;
THttpState = record
References: LongInt; // çağıran + son HANDLE_CLOSING callback'i
Event: THandle;
Status, Count: DWORD;
Buffer: array[0..16383] of Byte; // eşzamansız okumalar buraya düşer, asla stack'e değil
end;
procedure ReleaseState(State: PHttpState);
begin
if InterlockedDecrement(State.References) = 0 then
begin
CloseHandle(State.Event);
Dispose(State);
end;
end;
// Durum callback'inde: HANDLE_CLOSING, WinHTTP'nin istek için
// gönderdiği son bildirimdir, dolayısıyla ikinci referansı bırakır
if Status = HttpHandleClosing then
begin
ReleaseState(State);
Exit;
end;
libcurl'ün FPC Unix'te sağlaması gerekenler
Eşzamansız bir çözümleyici ve thread-safe bir derleme; yoksa çevrimiçi getiri kapalı kalır. FPC Unix'te taşıma libcurl üzerinden gider — Windows dışı hedefler için libcurl zaman damgası arka ucu makalesindeki bağımlılığın aynısı — ve bağlama, özellik maskesinde CURL_VERSION_ASYNCHDNS ya da CURL_VERSION_THREADSAFE'ten birini eksik olan hiçbir kütüphaneyi kabul etmez. Nedeni şu: başkasının süreci içinde yaşayan bir kütüphanenin kurmak zorunda olduğu CURLOPT_NOSIGNAL, eşzamansız çözümleyici olmadığında bir DNS aramasının zaman aşımını rahatça yaşatabilir. İkinci tuzak kapanıştır: curl_global_cleanup eşzamansız DNS thread'lerini beklemez; libcurl bir kez kurulduğunda modül, bir arka plan thread'inin kaldırılmış koda koşmasına izin vermek yerine süreç çıkana dek eşlenik kalır. İki gereklilikten biri karşılanmadığında SslCapabilities.OnlineRetrieval False olur ve SslVerifyOptionsDiagnostics, ağa danışılmış gibi yapmak yerine psvdOnlineRetrievalIgnored bildirir
Sonucun garantiledikleri ve garanti etmedikleri
Bu arka uçtan geçerli bir RevocationStatus, tüm zinciri kapsayan güncel CRL'lerin bulunduğunu, yapılandırıldığını ya da indirildiğini ve hiçbirinin zincirdeki bir sertifikayı listelemediğini demektir; daha fazlası değil. OCSP yoktur; iptali yalnızca OCSP üzerinden yayımlayan bir CA sonucu desteklenmez bırakır ve bir ağ başarısızlığı hiçbir şey yayımlamayan bir CA ile tam olarak aynı görünür. Ayrıca psvdNoCrlsConfigured, yalnızca sizin yapılandırdığınız CRL'leri betimler; çevrimiçi getiriyle bu bir ipucudur, başarısızlık tahmini değil. Denetim izinin ağ erişimi olmadan yeniden üretilebilir olması gerekiyorsa NetworkPolicy'yi ptnpOffline varsayılanında bırakın: hiçbir getiri oturumu kurulmaz ve arka uç hiçbir bağlantı açmaz; bu, Windows'ta çevrimdışı PDF imza iptal denetimleri makalesinde anlatılan CryptoAPI tarafındaki çevrimdışı sözleşmeyle aynıdır
Getiri kodu, bütçeler ve taşıma bağlamaları, PDFium Delphi component ile kaynak olarak gelir; böylece güvenilmeyen belgeleri işleyen bir sunucuda ptnpOnline'ı etkinleştirmeden önce bir doğrulamanın hangi URL'lere başvurabileceğini ve ne kadar indirebileceğini tam olarak teyit edebilirsiniz