Teknik Makale

Delphi'de PDF İmzaları için OpenSSL AIA ve CRL Getirmesi

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

PDFium Component OpenSSL arka ucunda VerifyCmsWithSsl sırası: CMS imza denetimi zincir değerlendirmesi bastırılmış olarak koşar, böylece bozuk baytlar ağa asla dokunmaz; RetrieveIntermediates, OnlineRetrieval etkinken yalnızca bir zincir başarısızlığından sonra AIA caIssuers URL'lerini izler ve RetrieveCrls, zincir güvenilir hale geldiğinde dağıtım noktası CRL'lerini ayrı bir depoya getirir
Bütünlük, sonra güven, sonra iptal: ağa yalnızca gerektiren adımların arasında dokunulur ve güvenilmeyen bir yola asılı bir CRL hiçbir şey kanıtlamaz
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; hem TPdfCmsVerifyOptions.Default hem TPadesTrustValidationOptions.Default iç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
PDFium Component'te bir doğrulama çağrısının AIA ve CRL adımlarının paylaştığı tek TPdfCryptoFetchSession'ın katı tavanları: UrlRetrievalTimeoutMs 15000 ms, sıfırdan 30000'e döner; oturum başına en çok 8 istek, yanıt başına 1 MiB ve toplamda 4 MiB — başarısız yanıtlar da sayar; 2048 karakterin üzerinde URL reddedilir
Sınırlar sabittir, öneri değil: başarısız bir yanıttan gelen baytlar da bütçeyi tüketir ve her imza ile zaman damgası ayrı doğrulandığı için en kötü durum imza sayısıyla büyür

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

Zaman aşımına uğramış bir WinHTTP isteği belleğe neden hâlâ yazabilir: zaman aşımında dönmek callback'leri havada bırakır; PDFium Component bu yüzden eşzamansız okumayı, 16 KB tamponu ve iki referansı olan yığında ayrılmış bir THttpState kaydına yönlendirir; biri çağıranın biri son HANDLE_CLOSING callback'inin bıraktığı referanslar yalnızca son taraf bitirdiğinde boşaltılır
Geç bir tamamlama, sizin bekleyişiniz vazgeçtikten sonra okumasını bitirebilir; iki referanslı yığın sahipliği, o yazının hâlâ yaşayan belleğe düşmesi demektir
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