Artikel Teknis

Retrieval AIA dan CRL OpenSSL untuk Signature PDF di Delphi

PDFium VCL kini bisa melengkapi rantai signature PDF dan memeriksa revocation lewat jaringan di backend OpenSSL-nya: saat OnlineRetrieval diaktifkan, ConfigureSslCmsVerifier memasang verifier yang mengunduh sertifikat intermediate yang hilang dari URL caIssuers AIA dan CRL dari CRL distribution point, di dalam anggaran waktu, permintaan, dan byte tetap per panggilan verifikasi. Sertifikat yang diunduh hanya pernah jadi material rantai. Trust tetap datang eksklusif dari system store dan anchor yang Anda konfigurasikan

Celah yang ditutup ini muncul pertama kali Anda memvalidasi PDF dunia nyata di server Linux. Porsi besar signer hanya menempelkan sertifikat leaf mereka sendiri di CMS, sehingga OpenSSL tak bisa mencapai root, TrustStatus kembali invalid, dan revocation tak pernah berjalan karena rantainya tak pernah jadi terpercaya. Sebelum v3.121.0, backend OpenSSL yang dijelaskan di memverifikasi signature PDF dengan OpenSSL di PDFium VCL benar-benar offline dan OnlineRetrieval tidak berefek apa pun padanya. Satu hal layak ditegaskan di muka: engine PDFium sendiri sama sekali tidak melakukan verifikasi CMS, jadi semua aturan di bawah ini tinggal di lapisan PAdES milik komponen dan binding OpenSSL-nya, tempat Anda bisa membacanya

Dalam urutan apa backend OpenSSL memverifikasi, mengambil, dan memeriksa?

Integritas dulu, lalu trust, lalu revocation, dan jaringan hanya disentuh di antara langkah-langkah yang membutuhkannya. VerifyCmsWithSsl memeriksa signature CMS dan signed attributes (RFC 5652) dengan evaluasi rantai disupresi, dan kalau itu gagal ia langsung kembali, sebelum sesi fetch pun ada, jadi dokumen dengan byte rusak tidak memicu permintaan keluar. Hanya kalau rantai lalu gagal dan OnlineRetrieval menyala barulah ia mengikuti link AIA dan memverifikasi lagi. CRL distribution point hanya di-fetch setelah rantai terpercaya, karena CRL yang menggantung di jalur tak terpercaya tidak membuktikan apa pun. Ketiga vonis tetap terpisah sepanjang jalan: signature valid dengan rantai tak lengkap tetap dilaporkan sebagai signature valid

Urutan VerifyCmsWithSsl di backend OpenSSL PDFium Component: pemeriksaan signature CMS berjalan dengan evaluasi rantai disupresi, jadi byte yang rusak tak pernah menyentuh jaringan; RetrieveIntermediates mengikuti URL caIssuers AIA hanya setelah kegagalan rantai dengan OnlineRetrieval aktif, dan RetrieveCrls meng-fetch CRL distribution point ke store terpisah begitu rantai terpercaya
Integritas, lalu trust, lalu revocation: jaringan hanya disentuh di antara langkah yang membutuhkannya, dan CRL yang menggantung di jalur tak terpercaya tak membuktikan apa pun
uses
  PDFium, FPdfCrypto, FPdfCryptoSsl, FPdfPades;

var
  Pdf: TPdf;
  Probe: TPdfCmsVerifyOptions;
  Diags: TPdfSslVerifyDiagnostics;
  Trust: TPadesTrustValidationOptions;
  Verdict: TPadesValidationResult;
  I: Integer;
begin
  ConfigureSslTrustAnchors(LoadCorporateRoots);   // DER; satu-satunya trust ekstra
  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 secara default
  Trust.NetworkPolicy := ptnpOnline;
  Trust.CheckRevocation := True;
  Trust.UrlRetrievalTimeoutMs := 10000;           // per panggilan verifikasi

  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;

Mengapa sertifikat yang diunduh tidak pernah masuk trust store?

Karena URL-nya datang dari sertifikat yang sedang divalidasi, dan signer-lah yang memilihnya. Entry authorityInfoAccess caIssuers (RFC 5280 §4.2.2.1) adalah petunjuk tentang di mana issuer tinggal, tidak lebih. Kalau apa pun yang menjawab URL itu masuk ke store trust-anchor, siapa pun bisa menandatangani dengan key buatan sendiri, mengarahkan AIA ke servernya sendiri, dan menerima vonis hijau. Karena itu RetrieveIntermediates menyerahkan tiap sertifikat yang ter-parse ke CMS_add1_cert, yang menempatkannya di set tak terpercaya milik struktur CMS ini saja, dan OpenSSL tetap harus membangun jalur dari sana ke anchor yang Anda konfigurasikan atau yang sudah dipegang system store. Ada alasan yang lebih senyap juga: argumen sertifikat milik CMS_verify bukan pengganti siap pakai untuk sertifikat yang tertanam di CMS, jadi menambah ke CMS itu sendiri adalah jalannya yang andal

Loop retrieval-nya sengaja dibuat sempit. RetrieveIntermediates berjalan paling banyak 4 ronde, tiap ronde mengumpulkan URL caIssuers dari setiap sertifikat yang kini ada di CMS, dan berhenti begitu sebuah ronde tak menambah apa pun atau anggaran waktunya habis. Respons harus ter-decode dengan d2i_X509 sebagai satu sertifikat DER tunggal yang menghabiskan seluruh body; byte ekor ditolak, dan bundle PKCS#7 certs-only yang disajikan dari URL .p7c dilewati alih-alih dibongkar. Access method OCSP di ekstensi AIA yang sama diabaikan, karena backend ini tidak bicara OCSP. Di sisi revocation, RetrieveCrls hanya membaca URI fullName milik tiap DistributionPoint (RFC 5280 §4.2.1.13) dari sertifikat CMS dan anchor yang dikonfigurasikan, dan CRL yang diunduh masuk ke X509_STORE kedua yang independen dengan pemeriksaan CRL full-chain, jadi CRL yang hilang atau basi mengubah RevocationStatus tanpa pernah menyentuh TrustStatus

// Dipadatkan dari VerifyCmsWithSsl (FPdfCryptoSsl.pas); setup BIO dihilangkan.
// Setiap panggilan _CMS_verify mendapat content BIO segar
if _CMS_verify(Cms, nil, nil, Bio, nil,
  CMS_NO_SIGNER_CERT_VERIFY or CMS_BINARY) <> 1 then
  Exit;                                   // signature rusak: tanpa jaringan sama sekali
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, hanya untrusted
  // verifikasi rantai lagi terhadap store anchor yang sama
end;

if Options.CheckRevocation and (FetchSession <> nil) and
   (Result.TrustStatus = pcvsValid) then
  RetrievedCrls := RetrieveCrls(Cms, FetchSession);
// store kedua, terpisah: CRL terkonfigurasi plus yang diambil
Store := BuildStore(True, RetrievedCrls, RevocationChecked, CrlsMalformed);

Berapa biaya maksimum satu panggilan verifikasi?

Plafon tetap, ditegakkan oleh satu TPdfCryptoFetchSession yang dibagi langkah AIA dan CRL dari satu panggilan verifikasi. Batas-batasnya adalah konstanta di FPdfCryptoHttp, bukan saran:

  • Waktu: UrlRetrievalTimeoutMs, yang default-nya 15000 di TPdfCmsVerifyOptions.Default maupun TPadesTrustValidationOptions.Default; sesi yang dibuat dengan 0 jatuh ke 30000, dan jamnya mulai berjalan begitu signature lolos, mencakup setiap permintaan sesudahnya
  • Permintaan: paling banyak 8 per sesi, dihitung sebelum transport dicoba, jadi host mati tetap menghabiskan satu slot
  • Byte: 1 MiB per respons dan 4 MiB total, dengan URL lebih panjang dari 2048 karakter ditolak sebelum koneksi apa pun
Plafon kaku satu TPdfCryptoFetchSession yang dibagi langkah AIA dan CRL sebuah panggilan verifikasi di PDFium Component: UrlRetrievalTimeoutMs default 15000 ms dengan fallback nol 30000, paling banyak 8 permintaan per sesi, 1 MiB per respons dan 4 MiB total dengan respons gagal tetap terhitung, dan URL di atas 2048 karakter ditolak
Batasnya konstanta, bukan saran: byte dari respons yang gagal tetap menguras anggaran, dan karena tiap signature dan timestamp diverifikasi terpisah, kasus terburuknya tumbuh dengan jumlah signature

Pembukuiannya lebih ketat daripada kelihatannya. Byte yang diterima dari respons gagal tetap terhitung ke total, jadi server yang menjawab 404 dengan halaman besar tak bisa menguras anggaran cuma-cuma. Pembacaan yang melintasi batas per-respons menggugurkan unduhan alih-alih meneruskan body yang terpotong ke parser ASN.1, dan HTTP 200 dengan body kosong ditolak mentah-mentah, karena jalur AIA kalau tidak akan mengindeks Data[0] milik array kosong. Hanya URL http:// dan https:// polos yang lolos, tanpa redirect, cookie, kredensial, atau penemuan proxy otomatis, sementara HTTPS mempertahankan pemeriksaan sertifikat dan hostname normalnya. Deduplikasi URL sengaja dicakupkan ke satu panggilan: validasi berikutnya harus bisa melihat CRL yang baru terbit. Anggarannya juga per panggilan, bukan per dokumen, dan ValidatePadesTrust memverifikasi tiap signature dan tiap token timestamp secara terpisah, jadi kasus terburuk tumbuh dengan jumlah signature

Mengapa permintaan WinHTTP yang timeout masih bisa menulis ke memori Anda?

Karena kembali saat timeout tidak membatalkan callback yang sudah di jalan. Transport Windows menggerakkan WinHTTP secara asinkron dan menunggu event dengan sisa waktu sesi, dan saat tunggu itu menyerah, permintaan masih bisa menyelesaikan sebuah read dan memberi sinyal sesudahnya. Arahkan read asinkron itu ke buffer stack, dan completion yang terlambat itu menulis ke frame yang saat itu sudah jadi milik fungsi tak berhubungan. Fix-nya adalah kepemilikan, bukan timing: event dan buffer read 16 KB tinggal di record heap dengan dua referensi, satu dipegang caller dan satu dilepas hanya oleh callback HANDLE_CLOSING terakhir, jadi sisi mana pun yang selesai terakhir membebaskan memorinya

Mengapa permintaan WinHTTP yang timeout masih bisa menulis ke memori: kembali saat timeout membiarkan callback di jalan, jadi PDFium Component mengarahkan read asinkron ke record THttpState yang dialokasikan di heap, yang buffer 16 KB dan dua referensinya — satu dipegang caller dan satu dilepas callback HANDLE_CLOSING terakhir — hanya dibebaskan saat sisi terakhir selesai
Completion yang terlambat bisa menuntaskan read-nya setelah tunggu Anda menyerah; kepemilikan heap dengan dua referensi berarti tulisan itu mendarat di memori yang masih hidup
type
  PHttpState = ^THttpState;
  THttpState = record
    References: LongInt;               // caller + callback HANDLE_CLOSING final
    Event: THandle;
    Status, Count: DWORD;
    Buffer: array[0..16383] of Byte;   // read asinkron mendarat di sini, tak pernah di stack
  end;

procedure ReleaseState(State: PHttpState);
begin
  if InterlockedDecrement(State.References) = 0 then
  begin
    CloseHandle(State.Event);
    Dispose(State);
  end;
end;

// Di status callback: HANDLE_CLOSING adalah notifikasi terakhir WinHTTP
// yang dikirim untuk request, jadi ia menjatuhkan referensi kedua
if Status = HttpHandleClosing then
begin
  ReleaseState(State);
  Exit;
end;

Apa yang harus disediakan libcurl di FPC Unix

Resolver asinkron dan build yang thread-safe, atau online retrieval tetap mati. Di FPC Unix transportnya lewat libcurl, dependensi yang sama di balik backend timestamp libcurl untuk target non-Windows, dan binding menolak library mana pun yang feature mask-nya tak memiliki CURL_VERSION_ASYNCHDNS atau CURL_VERSION_THREADSAFE. Alasannya, CURLOPT_NOSIGNAL — yang harus diset library di dalam proses orang lain — dikombinasikan dengan resolver sinkron berarti lookup DNS bisa dengan tenang melampaui timeout-nya. Jebakan kedua adalah shutdown: curl_global_cleanup tidak menunggu thread DNS asinkron, jadi begitu libcurl terinisialisasi, module tetap terpetakan sampai proses keluar alih-alih membiarkan thread latar belakang berlari ke kode yang sudah di-unload. Saat salah satu syarat gagal, SslCapabilities.OnlineRetrieval False dan SslVerifyOptionsDiagnostics melaporkan psvdOnlineRetrievalIgnored alih-alih berpura-pura jaringan sudah dikonsultasikan

Apa yang dijamin dan tidak dijamin hasilnya

RevocationStatus valid dari backend ini berarti CRL terkini yang mencakup seluruh rantai ditemukan, dikonfigurasikan, atau diunduh, dan tak satu pun di antaranya mencantumkan sertifikat di rantai itu; tidak lebih. Tidak ada OCSP, jadi CA yang menerbitkan revocation hanya lewat OCSP membiarkan hasilnya unsupported, dan kegagalan jaringan tampak persis seperti CA yang tak menerbitkan apa pun. Catat juga bahwa psvdNoCrlsConfigured hanya mendeskripsikan CRL yang Anda konfigurasikan, jadi dengan online retrieval ia petunjuk, bukan ramalan kegagalan. Saat audit trail harus bisa direproduksi tanpa akses jaringan, biarkan NetworkPolicy di default ptnpOffline-nya: tidak ada sesi fetch yang dibuat dan backend tak pernah membuka koneksi, yang cocok dengan kontrak offline di sisi CryptoAPI yang dijelaskan di pemeriksaan revocation signature PDF offline di Windows

Kode retrieval, anggaran-anggarannya, dan binding transport dikirim sebagai source bersama PDFium Delphi component, jadi Anda bisa memastikan persis URL mana saja yang boleh dihubungi sebuah validasi dan seberapa banyak boleh diunduh sebelum Anda menyalakan ptnpOnline di server yang menangani dokumen tak terpercaya