Artikel Teknis

Pemeriksaan Revokasi Signature PDF Offline di Windows

PDFium VCL memeriksa revokasi signature PDF secara offline di Windows dengan menambahkan CERT_CHAIN_REVOCATION_CHECK_CACHE_ONLY ke pass revokasi CertGetCertificateChain, karena flag cache-only yang dipakai untuk pembangunan chain sama sekali tidak mencakup retrieval CRL atau OCSP. Sejak v3.119.1, panggilan ValidatePadesTrust offline tetap menjauh dari jaringan, dan hasil yang bersih menuntut bukti revokasi nyata per sertifikat. Sisa tulisan ini membahas kenapa kedua bagian kalimat itu sama-sama butuh perbaikan

Setup yang membongkar masalah ini biasa saja. Layanan validasi berjalan di host Windows yang terkunci rapat, TPadesTrustValidationOptions.NetworkPolicy diset ptnpOffline (yang juga default-nya), dan operator mengharapkan setiap jawaban datang dari cache sertifikat lokal. Lalu seseorang menyadari request keluar ke distribution point CA di log firewall, atau batch job yang menggantung selama UrlRetrievalTimeoutMs penuh 15000 ms di setiap signature. Tak ada satu bagian kode pun yang meminta jaringan. Windows pergi ke sana sendiri

Mengapa pembangunan chain offline tetap menarik CRL di Windows?

Karena CERT_CHAIN_CACHE_ONLY_URL_RETRIEVAL hanya membatasi retrieval URL yang dilakukan pembangunan chain: fetch issuer AIA, update root dan CTL. Dokumentasi Microsoft untuk CertGetCertificateChain menyatakan eksplisit bahwa flag itu tidak berlaku untuk pemeriksaan revokasi. Revokasi punya sakelarnya sendiri, CERT_CHAIN_REVOCATION_CHECK_CACHE_ONLY ($80000000), dan tanpa itu para revocation provider bebas mengunduh CRL atau mengirim request OCSP meski panggilan di sekelilingnya tampak offline. PDFium VCL kini meng-OR flag itu ke pass revokasi setiap kali OnlineRetrieval False, di atas flag chain, CERT_CHAIN_REVOCATION_CHECK_CHAIN_EXCLUDE_ROOT, dan CERT_CHAIN_REVOCATION_ACCUMULATIVE_TIMEOUT. Ini penting lebih dari sekadar latensi: request OCSP memberi tahu responder sertifikat mana yang sedang Anda periksa, dan persis itulah yang harusnya dihindari validator ber-air-gap

Dua sakelar independen menjaga validasi signature PDF offline di Windows: CERT_CHAIN_CACHE_ONLY_URL_RETRIEVAL hanya membatasi pembangunan chain seperti fetch issuer AIA dan root, sementara revokasi butuh CERT_CHAIN_REVOCATION_CHECK_CACHE_ONLY, karena tanpa itu provider tetap mengunduh CRL dan mengirim request OCSP yang membocorkan sertifikat mana yang sedang divalidasi
PDFium VCL meng-OR flag cache-only revokasi ke pass revokasi setiap kali OnlineRetrieval False, sehingga validasi trust ptnpOffline tetap menjauh dari jaringan untuk kedua pass
uses
  PDFium, FPdfCrypto, FPdfPades;

var
  Options: TPadesTrustValidationOptions;
  Report: TPadesValidationResult;
begin
  Options := TPadesTrustValidationOptions.Default; // ptnpOffline, 15000 ms
  Options.CheckRevocation := True;                 // False secara default
  Options.CheckTimeStamps := True;
  // Offline kini berarti offline juga untuk revokasi: respons CRL dan OCSP
  // dari cache saja, checkpoint pcvstOnlineRetrieval tidak dimunculkan
  Report := Pdf.ValidatePadesTrust(Options);
end;

Dua pembangunan chain, dua field error

Backend Windows membangun chain dua kali, dan tiap pembangunan kini memiliki slot error-nya sendiri. Panggilan CertGetCertificateChain pertama berjalan tanpa flag revokasi dan memberi makan CertVerifyCertificateChainPolicy dengan policy dasar, yang menghasilkan TrustStatus dan TrustError. Panggilan kedua menambahkan flag revokasi. Sebelum v3.119.1, kegagalan panggilan kedua menulis GetLastError ke TrustError, sehingga chain yang baru saja terverifikasi terpercaya bisa kembali tampak tak terpercaya gara-gara revocation provider tersendat. Perbaikannya membaca GetLastError seketika dan menyimpannya di TPdfCmsVerifyResult.RevocationError, membiarkan vonis pass pertama tak tersentuh. Dan kembalian True dari panggilan kedua juga tak diperlakukan sebagai sukses; itu hanya berarti Windows menyerahkan context chain yang layak diperiksa

Apa yang sebenarnya dibuktikan mask error trust nol?

Dengan sendirinya, tak membuktikan apa-apa. TrustStatus.dwErrorStatus agregat bernilai nol setelah pass revokasi hanya berarti tak ada bit error yang tersulut, dan chain yang tak satu pun elemennya membawa informasi revokasi bisa menghasilkan persis itu. Kode lama memetakan "tanpa bit revoked, tanpa bit unknown, tanpa bit offline" langsung ke valid, cara klasik validator melaporkan sertifikat yang tak diperiksa sebagai bersih. Rutinitas ReadWinRevocationEvidence yang baru menelusuri setiap simple chain dan setiap elemen, menolak struktur yang cbSize-nya terlalu kecil untuk dibaca dengan aman, dan melaporkan sukses hanya ketika setidaknya satu elemen non-root ada dan setiap elemen semacam itu membawa CERT_REVOCATION_INFO yang dwRevocationResult-nya nol

Penelusuran bukti yang dijalankan ReadWinRevocationEvidence pada tiap elemen chain Windows di Delphi: pRevocationInfo harus ada, cbSize harus cukup besar untuk dibaca, dwRevocationResult harus nol, dan elemen akhir dikecualikan hanya saat ditandai self-signed atau CA-trusted, sehingga mask error trust nol tak bisa lagi menyembunyikan sertifikat yang tak diperiksa
Vonis bersih menuntut setidaknya satu elemen non-root dan jawaban dari setiap elemen yang diwajibkan, dengan RevocationError mempertahankan DWORD provider mentah seperti CRYPT_E_REVOKED
// Diringkas dari penelusuran bukti: elemen dihitung hanya ketika
// revocation provider benar-benar menjawab untuknya
for J := 0 to ElementCount - 1 do
begin
  Element := Elements[J];
  ExcludedRoot := (J = ElementCount - 1) and
    ((Element^.TrustStatus.dwInfoStatus and
      (CERT_TRUST_IS_SELF_SIGNED or CERT_TRUST_IS_CA_TRUSTED)) <> 0);
  InfoPresent := (Element^.pRevocationInfo <> nil) and
    (Element^.pRevocationInfo^.cbSize >= SizeOf(TCERT_REVOCATION_INFO));
  if not ExcludedRoot then
  begin
    Inc(RequiredCount);
    if not InfoPresent or
      (Element^.pRevocationInfo^.dwRevocationResult <> 0) then
      Complete := False;
  end;
end;
Complete := Complete and (RequiredCount > 0); // chain berisi root saja tak membuktikan apa pun

Hasil provider dipertahankan mentah. RevocationError menampung DWORD dwRevocationResult persis seperti yang dikembalikan provider, mengutamakan error dari elemen yang di-revoke bila ada (CRYPT_E_REVOKED adalah $80092010), dan bitmask trust tidak pernah dihias menjadi kode error native. Pemetaan ke TPdfCmsRevocationReason sengaja kasar: pcrrCertificateRevoked dengan pcvsInvalid untuk revokasi eksplisit, pcrrChainUntrusted ketika chain gagal karena alasan yang tak berkaitan revokasi, dan pcrrUnknown untuk sisanya. Windows mungkin mencoba OCSP alih-alih CRL, jadi hasil offline atau unknown tidak diterjemahkan menjadi pcrrCrlExpired. Backend verifikasi CMS OpenSSL bisa membuat pembedaan khusus-CRL itu karena ia hanya pernah mengevaluasi CRL yang Anda serahkan, sementara backend SecTrust macOS membiarkan field di pcrrNone dan nol, yang berarti "tak ada diagnostik rinci", bukan "lolos"

Di mana pengecualian root berhenti

CERT_CHAIN_REVOCATION_CHECK_CHAIN_EXCLUDE_ROOT dengan sah melewati jangkar, karena tak ada yang menerbitkan CRL yang mencabut root terhadap dirinya sendiri. Jebakannya adalah memutuskan elemen mana yang root. PDFium VCL mengecualikan elemen terakhir sebuah simple chain hanya ketika dwInfoStatus-nya menandainya self-signed ($00000008) atau eksplisit CA-trusted ($00004000). Host offline sering tak bisa menarik issuer yang hilang, sehingga chain berakhir di intermediate; memperlakukan elemen terakhir chain parsial itu sebagai root akan diam-diam menjatuhkan satu-satunya sertifikat yang status revokasinya paling mungkin absen dari cache. Elemen itu tetap di set yang diwajibkan, tak punya jawaban provider, dan hasilnya tetap pcvsIndeterminate

Bagaimana hasil revokasi signature dan timestamp dipisahkan?

Sebagai field terpisah yang tak pernah saling menimpa. Validator PAdES memverifikasi CMS detached milik signature dokumen dan CMS attached milik token timestamp RFC 3161 dalam dua panggilan independen, dan v3.119.0 memberi masing-masing diagnostiknya sendiri di TPadesSignatureValidation: RevocationReason dan NativeRevocationError untuk signer, TimeStampRevocationReason dan NativeTimeStampRevocationError untuk TSA. Sertifikat TSA yang di-revoke karena itu tak bisa menyaru sebagai signer yang di-revoke, dan kegagalan timestamp tidak menghapus hasil integritas yang sudah terbentuk. Ketika CheckRevocation False, atau validasi tak pernah sampai ke tahap itu, field-field tetap pcrrNone dan 0, jadi selalu bacalah keduanya di samping RevocationStatus dan TimeStampRevocationStatus

Revokasi signature dan timestamp tetap terpisah di PDFium VCL: CMS detached milik signature dokumen mengisi RevocationReason dan NativeRevocationError, CMS attached milik token RFC 3161 mengisi TimeStampRevocationReason dan NativeTimeStampRevocationError, dan tahap yang tak diperiksa membiarkan pcrrNone dan nol di samping field statusnya
Sertifikat TSA yang di-revoke karena itu tak bisa menyaru sebagai signer yang di-revoke, dan kegagalan timestamp tak pernah menghapus hasil integritas yang sudah dibangun verifikasi signature
for I := 0 to High(Report.Signatures) do
begin
  S := Report.Signatures[I];
  case S.RevocationStatus of
    pcsInvalid:
      Log(Format('sig %d: signer revoked, provider 0x%.8x',
        [I, S.NativeRevocationError]));
    pcsIndeterminate:
      Log(Format('sig %d: revocation unknown, reason %d, provider 0x%.8x',
        [I, Ord(S.RevocationReason), S.NativeRevocationError]));
    pcsNotChecked:
      Log(Format('sig %d: revocation not checked', [I]));
  end;
  if S.TimeStampRevocationStatus = pcsIndeterminate then
    Log(Format('sig %d: TSA revocation unknown, provider 0x%.8x',
      [I, S.NativeTimeStampRevocationError]));
end;

Report bukti mengikuti aturan yang sama. Export CSV menambahkan revocationReason, nativeRevocationError, dan kolom-kolom timestamp sampai nativeTimeStampRevocationError di ujung urutan kolom yang ada, sehingga parser lama tetap bekerja, dan export JSON menambah field yang senada tanpa mengubah makna field lama. Kalau validasi offline terus kembali sebagai indeterminate, perbaikan yang tahan lama ada di hulunya: kumpulkan material validasi saat penandatanganan, seperti dibahas di signature PDF jangka panjang dengan timestamp RFC 3161 dan DSS, daripada berharap mesin verifikator punya cache yang hangat

Apa yang dibuktikan dan tidak dibuktikan matriks test

Matriks verifikasi Windows lolos 30 skenario chain-API terkendali dan satu smoke CMS offline sungguhan di tiap target Win32 dan Win64 Delphi serta FPC. Smoke sungguhan memverifikasi signature valid di bawah CA privat tak terpercaya, sementara hasil bersih dan yang eksplisit di-revoke berasal dari respons CertGetCertificateChain stub alih-alih trust anchor terpasang atau retrieval live. Itu batas jujur yang layak dinyatakan: penanganan flag, isolasi error, dan penelusuran bukti sudah dipatok, tapi isi cache revokasi mesin tertentu di hari tertentu tetap urusan Windows, dan cache kosong kini menghasilkan "unknown" dengan benar alih-alih request jaringan atau "valid" palsu

Penanganan revokasi offline, diagnostik per-field, dan export bukti adalah bagian dari API validasi signature PDF di PDFium VCL untuk Delphi dan C++Builder, berdampingan dengan backend OpenSSL dan macOS untuk deployment lintas platform