PDFium VCL menandatangani dokumen PAdES dengan private key yang tersimpan di Keychain macOS lewat backend yang me-resolve setiap simbol Security dan CoreFoundation saat runtime dengan dlopen dan dlsym. Tak ada yang terikat saat link, artinya nama simbol yang salah ketik muncul sebagai KeychainAvailable yang mengembalikan False dan KeychainMissingSymbols yang menyebut pelakunya, alih-alih linker error atau crash
Pilihan itu terpaksa karena constraint yang tak nyaman, dan cara penanganannya bisa dipinjam. Unit ini ditulis di mesin tanpa SDK macOS, sehingga setiap nama simbol framework dan setiap konstanta datang dari dokumentasi dan tak satu pun bisa diperiksa terhadap header. Respons yang salah atas situasi itu adalah menulis kodenya dengan hati-hati lalu berharap. Yang benar adalah menata agar kesalahan yang tak terelakkan mengumumkan dirinya sendiri dalam bentuk yang paling mudah dilokasikan
Mengapa dynamic binding adalah keputusan yang benar bahkan di platform target
Karena ia mengubah kelas kegagalan yang menghentikan program menjadi kelas kegagalan yang melaporkan dirinya sendiri. Referensi framework yang di-link statis dan salahnya gagal saat link di target dan tak pernah ter-link di tempat lain. Yang terikat dinamis dan salahnya menghasilkan backend yang tak tersedia plus daftar nama yang tak ter-resolve, dan run pertama di Mac mengubah pertanyaan dari mengapa ini tak tersedia menjadi satu baris yang menyebut salah ketik
Ada manfaat kedua yang terbayar setiap hari, bukan sekali. Karena unit ini tak me-link framework apa pun, ia terkompilasi di setiap platform, sehingga build Windows biasa terus memeriksa sintaksnya, tipenya, dan uses clause-nya. Unit yang hanya terkompilasi di platform yang tak dimiliki siapa pun di tim adalah unit tanpa kompilator yang melihatnya, dan ia meluruh diam-diam di setiap refactor tipe bersama
uses
FPdfCrypto, FPdfCryptoMac;
var
Options: TPadesSignerOptions;
begin
if not KeychainAvailable then
raise Exception.Create('Keychain backend unavailable, unresolved: ' +
KeychainMissingSymbols);
ConfigureKeychainSignerProvider; // pasang sebagai backend signer PAdES
ConfigureKeychainCmsVerifier; // dan sebagai backend verifikasi
Writeln('signer backend : ', PadesCryptoBackendName);
Writeln('verify backend : ', PadesCmsVerificationBackendName);
Options := TPadesSignerOptions.Default;
Options.CertificateThumbprint := 'B1 3F 9C ...'; // SHA-1, huruf apa pun
Options.PaddingScheme := psRsaPss;
end;
Dua jenis simbol terekspor, dua cara membacanya
Ini detail paling membingungkan di seluruh binding, dan menjalankannya terbalik terkompilasi bersih lalu gagal saat runtime. CoreFoundation dan Security mengekspor dua hal yang berbeda kategorial lewat panggilan dlsym yang sama, dan kodenya harus tahu mana yang mana
Konstanta bernama seperti key class item keychain dan singleton boolean CoreFoundation adalah variabel terekspor yang isinya adalah CFStringRef atau CFBooleanRef yang Anda cari. dlsym mengembalikan alamat variabel itu, jadi Anda harus melakukan dereference sekali untuk mendapat nilainya. Struktur tabel callback seperti callback key dan value dictionary adalah struktur terekspor, dan dlsym mengembalikan alamat strukturnya, yang persis pointer yang diharapkan fungsi pembuatan dictionary. Dereference yang satu ini dan Anda memberikan machine word pertama struktur seolah itu pointer
Tak satu pun kesalahan menghasilkan compile error, dan tak satu pun menghasilkan runtime error yang jelas. Anda mendapat pointer sampah yang gagal di suatu tempat di hilir. Cara membuat distingsi itu mustahil keliru adalah berhenti mengandalkan ingatan: dua fungsi helper, satu yang mengikat lalu melakukan dereference dan satu yang mengikat tanpa, sehingga call site mendeklarasikan jenis simbol mana yang dimintanya dan helper menegakkan sisanya
// Variabel terekspor: dlsym memberi alamat variabel yang menampung
// CFTypeRef, jadi dereference sekali
FSecClassKey := BindConstant(SecurityLib, 'kSecClass');
// Struktur terekspor: dlsym memberi alamat DARI struktur itu, dan itulah
// yang diinginkan API. Jangan di-dereference
FKeyCallbacks := BindStruct(CoreFoundationLib,
'kCFTypeDictionaryKeyCallBacks');
Mengapa tanda tangan RSA-PSS butuh dua fallback terpisah?
Karena algoritmanya bisa tidak ada dengan dua cara independen, dan hanya salah satunya pertanyaan versi. Konstanta algoritma digest-signing PSS muncul di macOS 10.13, jadi di sistem lebih tua simbolnya sekadar tidak ada dan bindingnya mendapat nil. Itu pemeriksaan versinya. Secara terpisah, di sistem tempat konstantanya ada, key tertentu bisa tetap menolaknya, dan framework menjawab pertanyaan itu lewat SecKeyIsAlgorithmSupported untuk key itu. Key yang ditopang perangkat keras atau key dengan atribut pembatas bisa menolak PSS sementara key perangkat lunak di mesin yang sama menerimanya
Kedua jalur harus bermuara ke fallback yang sama: beralih ke PKCS#1 v1.5. Dan bagian kritisnya, fallback itu juga harus mengubah identifier algoritma yang ditulis ke struktur CMS, bukan hanya panggilan penandatanganannya. Memancarkan identifier algoritma PSS sambil benar-benar menghasilkan tanda tangan v1.5 menghasilkan dokumen yang ditolak mentah-mentah oleh setiap verifier — secara mutlak lebih buruk daripada melaporkan bahwa PSS tak didukung. Downgrade itu dapat diterima, ketidakcocokan antara yang Anda deklarasikan dan yang Anda kerjakan tidak, dan itu aturan umum untuk kode tanda tangan, bukan keanehan macOS. Implikasi level tanda tangan dibahas di menandatangani PDF dengan PAdES B-B
Encoding tanda tangan ECDSA, dan pembalikan yang layak dicatat
Jalur kurva eliptik tak butuh konversi sama sekali di macOS, dan itu kebalikan dari yang diharuskan binding PKCS#11. Algoritma digest-signing framework Security untuk ECDSA mengembalikan tanda tangan yang sudah dalam bentuk X9.62 DER, persis yang diinginkan CMS. Token PKCS#11 mengembalikan pasangan P1363 lebar tetap mentah sebagai gantinya, yang harus dienkode ulang sebelum masuk ke struktur tanda tangan
Jadi dua backend yang mengimplementasikan interface yang sama butuh perlakuan berlawanan untuk algoritma yang sama, dan tak satu pun yang salah. Inilah persisnya jenis perbedaan yang harus diserap abstraksi alih-alih diekspos: lapisan PAdES meminta provider untuk menandatangani, dan konvensi encoding tinggal di dalam provider. Kalau mereka bocor ke atas, setiap pemanggil berakhir membawa conditional per backend. Bentuk yang sama muncul di kisah penandatanganan jarak jauh yang dibahas di sesi penandatanganan PAdES jarak jauh terhadap HSM
// Interface provider sama di setiap platform, jadi pemilihan adalah
// keputusan startup, bukan keputusan per pemanggilan
{$IFDEF DARWIN}
if KeychainAvailable then
ConfigureKeychainSignerProvider;
{$ENDIF}
{$IFDEF MSWINDOWS}
// Provider CNG Windows dipasang oleh unit platform
{$ENDIF}
if not PadesCryptoAvailable then
raise Exception.Create('no signing backend on this platform');
// Mulai dari sini kode penandatanganannya netral platform
Signer := ResolvePadesSigner(Options);
Aturan reference counting yang berjarak tiga baris
Manajemen memori Core Foundation mengikuti konvensi penamaan, dan jebakannya di sini adalah fungsi-fungsi dengan konvensi berbeda muncul berdampingan di blok pendek yang sama. Fungsi yang gets sertifikat dari trust object mengembalikan borrowed reference yang tidak boleh dilepas. Fungsi yang copy sertifikat signer atau menyalin datanya mengembalikan owned reference yang wajib dilepas. Tiga panggilan berurutan, dua aturan kepemilikan, dan melepas yang borrowed tidak gagal di baris itu. Ia merusak retain count lalu menjatuhkan sesuatu yang tak berkaitan belakangan
Mitigasinya adalah membaca verba di setiap nama fungsi framework sebelum menulis pembersihannya, setiap kali, tanpa kecuali. Ini padanan CoreFoundation dari memeriksa apakah sebuah API mengembalikan salinan atau view, dan biaya kelirunya adalah crash intermiten alih-alih error
Apa yang tidak diklaim backend ini
Ia belum pernah berjalan di macOS pada saat penulisan, dan mengatakannya dengan lugas lebih berguna daripada jaminan tersirat. Yang terbukti benar lebih sempit dan tetap bernilai: unit ini terkompilasi di Windows sebagai bagian dari build harian, setiap simbol framework diikat by name saat runtime dengan kegagalannya dienumerasi, dan logika pemilihan algoritma termasuk kedua fallback PSS adalah Pascal biasa yang bisa ditinjau dan dinalar. Run pertama di Mac akan bekerja atau menghasilkan daftar nama untuk diperbaiki
Padanan verifikasinya, yang memakai dekoder CMS tingkat lebih tinggi alih-alih merakit struktur CMS dengan tangan, dibahas di memverifikasi tanda tangan PDF di macOS dengan SecTrust, dan ia berbagi infrastruktur binding yang sama serta pendekatan diagnostik yang sama
Ide yang bisa dibawa pulang di sini soal penempatan risiko, bukan soal macOS. Ketika Anda harus menulis kode terhadap interface yang tak bisa Anda verifikasi, pilihlah konstruksi tempat kesalahan paling murah dilokasikan. Dynamic binding dengan daftar eksplisit nama yang tak ter-resolve mengubah dua puluh asumsi yang tak bisa diverifikasi menjadi satu baris diagnostik. Kedua backend dikirim sebagai source bersama komponen Delphi PDFium, jadi kalau ada nama simbol yang memang perlu dikoreksi, itu perubahan satu baris di tree Anda sendiri, bukan tiket dukungan