Artikel Teknis

Menandatangani PAdES dengan Keychain macOS di Delphi

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

Diagram backend Keychain macOS PDFium VCL yang me-resolve simbol Security dan CoreFoundation lewat dlsym: kSecClass adalah variabel terekspor yang di-dereference BindConstant sekali untuk mendapat nilai CFStringRef, sementara kCFTypeDictionaryKeyCallBacks adalah struktur terekspor yang diberikan BindStruct lewat alamat, dan mencampuradukkan kedua aturan menghasilkan pointer sampah di hilir
Satu panggilan dlsym mengembalikan dua hal yang berbeda kategorial: alamat variabel yang menampung CFTypeRef dan alamat struktur callback. Dua helper menjadikan keputusan dereference-atau-tidak di lokasi binding alih-alih di memori
// 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

Rantai keputusan yang menunjukkan mengapa penandatanganan RSA-PSS di backend Keychain PDFium VCL butuh dua fallback independen: dlsym mengembalikan nil untuk konstanta digest-signing di versi macOS sebelum 10.13, SecKeyIsAlgorithmSupported bisa menolak key yang ditopang perangkat keras, dan kedua gerbang bermuara ke downgrade PKCS#1 v1.5 yang sama yang identifier algoritma CMS-nya harus ikut berubah
PSS bisa tak tersedia dua kali lipat, sekali per versi macOS dan sekali per key, dan hanya gerbang versi yang pertanyaan sistem. Kedua gerbang bermuara ke downgrade v1.5 yang sama, dan identifier CMS mengikuti

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

Perbandingan encoding tanda tangan ECDSA di dua backend signer PAdES PDFium VCL: framework Security Keychain macOS mengembalikan X9.62 DER yang diterima CMS tanpa konversi, sementara token PKCS#11 mengembalikan pasangan P1363 lebar tetap mentah yang harus dienkode ulang, sehingga ResolvePadesSigner menjaga konvensi encoding di dalam provider
Interface ECDSA yang sama butuh perlakuan berlawanan per backend: Security menyerahkan DER yang jadi sementara token PKCS#11 menyerahkan P1363 mentah, jadi konversinya tinggal di dalam provider dan pemanggil tak pernah melihat conditional per backend
// 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