Artikel Teknis

Memverifikasi Tanda Tangan PDF dengan OpenSSL di PDFium VCL

PDFium VCL memperlakukan verifikasi CMS sebagai backend yang bisa diganti di balik interface IPdfCmsVerifier, sehingga validator PAdES bisa berjalan di Windows lewat CryptoAPI, di macOS lewat Keychain, dan di mana pun OpenSSL ada lewat ConfigureSslCmsVerifier. Interfacenya kecil. Tiga perilaku OpenSSL di bawahnya menghasilkan jawaban yang salah dengan penuh keyakinan bila Anda mengimplementasikannya secara naif

Motivasinya cukup jelas begitu aplikasi Delphi meninggalkan Windows. Validasi tanda tangan adalah salah satu dari sedikit area tempat stack kripto platform bukan sekadar detail implementasi: ia memutuskan sertifikat mana yang dipercaya, algoritma mana yang ada, dan apa arti revocation. Hard-code satu dan kodenya tidak bisa di-port. Abstraksi dengan buruk dan setiap platform melaporkan jawaban berbentuk berbeda yang tak bisa dibandingkan pemanggilnya

Apa yang sebenarnya harus diangkut abstraksi ini

Dua bentuk verifikasi dan tiga verdict independen. Tanda tangan PDF bersifat detached: konten yang ditandatangani adalah dua rentang byte di kedua sisi lubang /Contents, jadi VerifyDetached menerima dua segmen alih-alih satu buffer. Token timestamp bersifat attached, membawa kontennya sendiri, jadi VerifyAttached hanya menerima DER-nya

Hasilnya terbelah menjadi tiga status karena ketiganya menjawab tiga pertanyaan berbeda dan boleh saja tidak sepakat. SignatureStatus menyatakan apakah byte-byte itu ditandatangani kunci di sertifikat signer. TrustStatus menyatakan apakah sertifikat itu berantai ke sesuatu yang Anda percaya. RevocationStatus menyatakan apakah sertifikatnya masih sah pada waktu yang relevan. Dokumen dengan tanda tangan yang sempurna secara matematis dari sertifikat yang tak pernah Anda dengar itu valid, tak terpercaya, dan tak diketahui — dan menciutkannya menjadi satu boolean adalah cara validator berakhir berbohong kepada pengguna

uses
  FPdfCrypto, FPdfCryptoSsl;

var
  Options: TPdfCmsVerifyOptions;
begin
  if not SslAvailable then
    raise Exception.Create('libcrypto not usable: ' + SslMissingSymbols);

  ConfigureSslTrustAnchors(LoadCorporateRoots);   // DER, boleh kosong
  ConfigureSslCrls(LoadFreshCrls);                // DER, boleh kosong
  ConfigureSslCmsVerifier;                        // memasang backend

  Writeln('backend  : ', PadesCmsVerificationBackendName);
  Writeln('library  : ', SslLibraryPath, ' ', SslLibraryVersion);
  Writeln('ABI      : ', SslAbiLayout);           // ulong=<n> long=<n>

  Options := TPdfCmsVerifyOptions.Default;
  Options.CheckRevocation := True;
  Options.CollectChainCertificates := True;
end;

SslAbiLayout tampak seperti keingintahuan belaka dan bukan. Setiap kode error OpenSSL dan setiap flag store menyeberangi batas sebagai C unsigned long, yang empat byte di Windows dan delapan di Linux dan macOS. Deklarasikan sebagai tipe 32-bit tetap dan kodenya bekerja di Windows, lalu diam-diam membaca separuh nilai di LP64. Melaporkan lebar yang diasumsikan sebagai string yang bisa Anda assert dalam test mengubah seluruh kelas penyimpangan ABI platform menjadi pemeriksaan satu baris. Siapa pun yang pernah bergulat dengan masalah yang sama pada CK_ULONG di binding PKCS#11 akan langsung mengenalinya; kisahnya ada di struct packing PKCS#11 dan lebar CK_ULONG

Mengapa pass verifikasi kedua melihat konten kosong?

Karena CMS_verify membaca BIO konten detached sampai akhir file, dan BIO yang sudah dibaca tidak di-rewind untuk Anda. Memverifikasi dalam dua pass adalah desain yang wajar — dulu tanda tangan kriptografinya saja dengan evaluasi chain ditekan, lalu evaluasi penuh — dan ia gagal dengan cara yang luar biasa menipu bila kedua pass berbagi satu BIO

Pass kedua mendapat nol byte konten. Dalam mode detached itu bukan error, karena buffer konten kosong adalah input yang sah. Digestnya sekadar tidak cocok, dan kegagalannya muncul sebagai kegagalan membangun chain alih-alih kegagalan konten, yang mengarahkan Anda memeriksa sertifikat dan trust store padahal masalah sebenarnya adalah posisi stream. Bangun ulang memory BIO dengan BIO_new_mem_buf untuk setiap pass. Biayanya satu alokasi dan kemungkinan itu hilang sepenuhnya

Apa yang ditekan dan tidak ditekan flag no-verify

CMS_NO_SIGNER_CERT_VERIFY menekan evaluasi chain, bukan pencarian sertifikat signer. Secara internal OpenSSL me-resolve dan melampirkan sertifikat signer sebelum ia berkonsultasi ke flag itu, sehingga setelah pass pertama yang membawa flag tersebut, signer sudah tersedia dan identifier algoritmanya bisa dibaca begitu saja. Tak perlu menjalankan verifikasi penuh kedua hanya untuk mendapat sertifikat signer — itulah asumsi yang menggoda dari namanya

Satu aturan kepemilikan menyertai itu. Referensi signer milik struktur CMS dan tidak boleh dibebaskan sendirian. Ia sah selama strukturnya sah, dan membebaskannya menghasilkan korupsi yang gejalanya muncul di tempat yang sama sekali lain, biasanya saat pembersihan objek yang tak berkaitan

Mengapa menyalakan pemeriksaan CRL menolak setiap tanda tangan?

Karena OpenSSL memeriksa CRL hanya terhadap apa yang sudah dipegang store dan tidak mengambil apa pun dengan sendirinya. Ia tidak mengikuti CRL distribution point dan tidak berbicara OCSP. Setel X509_V_FLAG_CRL_CHECK pada store yang tak berisi CRL dan setiap chain gagal dengan ketidakmampuan memperoleh CRL sertifikat. Hasilnya tampak seperti pemeriksaan revocation yang bekerja dan menemukan masalah. Padahal itu pemeriksaan revocation yang tidak pernah berjalan sama sekali

Karena itu backend hanya menyetel flag itu ketika ConfigureSslCrls benar-benar menyuplai sekurangnya satu CRL. Tanpa satu pun, RevocationStatus kembali sebagai pcvsUnsupported, pernyataan jujur bahwa pertanyaannya tidak terjawab. Dengan alasan yang sama OnlineRetrieval tidak berefek pada backend ini dan checkpoint pcvstOnlineRetrieval tidak dipancarkan: tak ada jalur pengambilan untuk melaporkan kemajuannya

Diagram verifier CMS OpenSSL PDFium VCL atas tiga jebakan: BIO konten yang dibagi bersama dan terbaca sampai akhir file menyisakan pass verifikasi kedua dengan nol byte, CMS_NO_SIGNER_CERT_VERIFY menekan evaluasi chain tapi bukan pencarian signer, dan pemeriksaan CRL pada store kosong menolak setiap chain tanpa revocation pernah berjalan
Tiap jebakan menghasilkan verdict salah yang penuh keyakinan: posisi stream menyamar sebagai kegagalan trust, flag no-verify menekan lebih sedikit dari yang namanya sarankan, dan revocation yang tak pernah berjalan tampak seperti revocation yang menemukan masalah

Ini posisi desain yang layak dipertahankan secara umum. Validator yang tak bisa memeriksa revocation seharusnya mengatakannya. Melaporkan sertifikat yang tak diperiksa sebagai tidak dicabut adalah cara paling umum tool validasi tanda tangan menyesatkan penggunanya, dan itulah persisnya kelas kebingungan yang dieksplorasi di mengapa validator menolak tanda tangan PAdES

// Checkpoint memungkinkan UI menampilkan stage mana yang berjalan, dan
// memberi tahu stage mana yang benar-benar dijalankan sebuah backend
type
  TSignatureProbe = class
    procedure Checkpoint(Stage: TPdfCmsVerifyStage);
  end;

procedure TSignatureProbe.Checkpoint(Stage: TPdfCmsVerifyStage);
begin
  case Stage of
    pcvstCryptographicSignature: Status('checking the signature');
    pcvstChainBuild:             Status('building the certificate chain');
    pcvstOnlineRetrieval:        Status('fetching validation data');
    pcvstRevocationCheck:        Status('checking revocation');
  end;
end;

// Baca ketiga verdict secara terpisah; mereka boleh tidak sepakat
if Result.SignatureStatus = pcvsValid then
  case Result.TrustStatus of
    pcvsValid:         Report('signed and trusted');
    pcvsInvalid:       Report('signed, chain rejected');
    pcvsUnsupported,
    pcvsIndeterminate: Report('signed, trust not established');
  end;
if Result.RevocationStatus = pcvsUnsupported then
  Report('revocation was not checked on this backend');

Mengikat ke pustaka yang tak bisa Anda pin

OpenSSL mengganti nama accessor stack-nya antara 1.0 dan 1.1, sehingga fungsi logis yang sama punya dua nama ekspor yang mungkin bergantung pada build yang kebetulan dimiliki host. Bindingnya me-resolve nama yang lebih baru lebih dulu dan jatuh ke yang lebih tua, dan hanya mencatat simbol yang hilang ketika keduanya tak ter-resolve. Itu bentuk yang benar untuk binding dinamis apa pun terhadap pustaka yang tidak Anda kirim: utamakan nama terkini, toleransi nama historis, dan laporkan hanya ketidakhadiran yang sungguhan

SslMissingSymbols adalah yang mengubah pemuatan yang gagal menjadi kejadian yang bisa didiagnosis. Hasil tak kosong pada host yang jelas-jelas memasang libcrypto berarti versi terpasangnya lebih tua dari API yang dituju build ini — percakapan dukungan yang sama sekali berbeda dari pustaka yang tidak ada. ConfigureSslLibraryPath mencakup kasus umum lainnya: host dengan beberapa build OpenSSL tempat yang berada di default search path bukan yang Anda inginkan

Memilih backend per platform

Susunan praktisnya adalah memilih saat startup dan mencatat yang mana yang menjawab. Di Windows, backend platform terintegrasi dengan certificate store yang sudah dikelola enterprise, yang biasanya memang yang Anda inginkan. Di macOS backend Keychain cocok dengan nalar yang sama dan dibahas di memverifikasi tanda tangan dengan SecTrust di macOS. OpenSSL adalah opsi portabel, dan ia juga pilihan yang tepat ketika Anda butuh kebijakan validasi yang identik lintas platform alih-alih yang mengikuti trust store tiap platform

Diagram PDFium VCL atas abstraksi IPdfCmsVerifier yang mengangkut VerifyDetached atas dua rentang byte di sekeliling lubang Contents dan VerifyAttached untuk token timestamp, tiga verdict independen SignatureStatus, TrustStatus dan RevocationStatus, serta backend per platform yang dipilih saat startup lewat CryptoAPI, SecTrust atau ConfigureSslCmsVerifier
Interface mengangkut dua bentuk verifikasi dan tiga verdict karena ketiganya menjawab pertanyaan berbeda dan boleh tidak sepakat, dan backend yang terpasang dicatat di samping setiap verdict agar hasil tersimpan bisa direproduksi

Apa pun yang Anda pasang, catat PadesCmsVerificationBackendName di samping setiap verdict yang Anda rekam. Hasil validasi tersimpan tanpa backend yang menghasilkannya tak bisa direproduksi belakangan, karena ketiga nilai status itu bermakna halus berbeda bergantung pada stack mana yang menjawab. Lapisan inspeksi tanda tangan di atas semua ini, termasuk bagaimana level PAdES dilaporkan, dibahas di menginspeksi tanda tangan digital PDF dan level PAdES

Semuanya dikirim sebagai source bersama komponen Delphi PDFium, dan di sini itu lebih penting daripada biasanya: bagi validator tanda tangan, bisa membaca persis flag mana yang disetel sebuah backend dan pemeriksaan mana yang dilewatinya bukan bonus, melainkan satu-satunya cara mengetahui apa yang sebenarnya diklaim tanda centang hijau di aplikasi Anda