PDFium Delphi Component memverifikasi signature PDF di macOS melalui TPdfKeychainCmsVerifier, CMS verification backend yang dibangun di atas Apple CMSDecoder dan SecTrust, bukan CMS yang diparse manual. ConfigureKeychainCmsVerifier memasangnya, dan satu pemanggilan CMSDecoderCopySignerStatus mengembalikan signature verdict, SecTrust handle, serta certificate result code, tepat pasangan kolom yang sudah dibawa TPdfCmsVerifyResult di Windows
Skenario yang memaksa pekerjaan ini membosankan dan umum. Lazarus build untuk document archive berjalan di Mac, membuka signed contract, dan setiap signature kembali sebagai pcsUnsupported. Tidak ada yang salah pada file. Signature verification hanya belum memiliki backend di luar Windows, dan PAdES validator menolak menebak ketika backend tidak ada. PDFiumPas version 3.111.0 membuka seam dengan IPdfCmsVerifier dan ConfigurePadesCmsVerifier; version 3.113.0 mengisinya di macOS. Bagian menarik dari port ini bukan plumbing-nya, melainkan tiga tempat ketika Apple API tidak memiliki bentuk yang sama dengan API Windows
Mengapa PDF signature mencakup dua byte range?
Karena signature tidak dapat mencakup byte yang menyimpannya sendiri. ISO 32000-1 §12.8.1 menempatkan CMS SignedData blob di dalam string /Contents milik signature dictionary dan menjelaskan extent yang ditandatangani dengan /ByteRange, yaitu pasangan offset dan length yang mencakup semua byte di kedua sisi lubang tersebut. Dua segment, satu gap di tengah, pada setiap platform
Platform tidak sepakat tentang cara segment tersebut mencapai crypto layer, dan ketidaksepakatan itu menghabiskan memory. Di Windows, CryptVerifyDetachedMessageSignature menerima array pointer dan length, sehingga kedua span diberikan sesuai letaknya di buffer dan tidak ada yang diduplikasi. Apple CMSDecoderSetDetachedContent menerima satu CFData dan tidak memiliki bentuk multi-segment, sehingga macOS backend menggabungkan dua range menjadi contiguous buffer sebelum decoding. Itu adalah full second copy dari signed byte. Pada scanned archive berukuran 400 MB, ini menjadi memory peak yang nyata, skalanya mengikuti document, bukan signature, dan tidak ada API alternatif yang dapat dipakai. Ukur batch worker dengan semestinya, bukan menemukannya di mesin customer
Satu pemanggilan mengisi dua kolom TPdfCmsVerifyResult
CMSDecoderCopySignerStatus sangat dermawan untuk Security.framework entry point: satu pemanggilan mengembalikan signer status, SecTrustRef untuk chain yang dibangunnya, dan OSStatus untuk certificate evaluation. Nilai tersebut langsung masuk ke record yang sudah dikonsumsi PAdES validator, dengan signer status menjadi SignatureStatus, certificate result menjadi TrustStatus, dan raw value dipertahankan dalam SignatureError serta TrustError agar support ticket dapat mengutip angka, bukan kata sifat. Caller tidak pernah menyentuh IPdfCmsVerifier sendiri — ValidatePadesCompliance dan ValidatePadesTrust mengarahkan setiap verification melalui backend mana pun yang terpasang, sehingga code yang membaca TPadesSignatureValidation byte-for-byte sama pada kedua platform, sebagaimana dijelaskan dalam walkthrough memeriksa signature dictionary PDF dan level PAdES di Delphi
uses
FPdfCrypto, FPdfCryptoMac, FPdfPades;
procedure InstallMacVerifier;
begin
// Signing dan verification me-resolve framework symbol berbeda, sehingga satu
// bisa ada sementara yang lain tidak
if not KeychainVerificationAvailable then
raise Exception.CreateFmt('Security.framework symbols missing: %s',
[KeychainMissingSymbols]);
ConfigureKeychainCmsVerifier;
// PadesCmsVerificationBackendName kini menjawab 'macOS Security.framework'
if not PadesCmsVerificationAvailable then
raise Exception.Create('No CMS verification backend is installed');
end;
Mengapa kCMSSignerInvalidCert melaporkan signature yang valid?
Karena Apple memberi value tersebut arti yang lebih sempit daripada kesan namanya: signature berhasil diverifikasi dan hanya certificate chain yang tidak dapat dibangun. Karena itu TPdfKeychainCmsVerifier memetakan kCMSSignerInvalidCert menjadi pcvsValid pada kolom SignatureStatus dan membiarkan masalah certificate muncul melalui TrustStatus, tempat masalah chain memang berada. Melipatnya ke dalam signature verdict akan membuat component memberi tahu operator bahwa dokumen yang tidak diubah telah dimodifikasi, dan itulah false alarm terburuk yang dapat diangkat signature validator
function MapSignerStatus(Status: LongWord): TPdfCmsVerifyStatus;
begin
case Status of
kCMSSignerValid:
Result:= pcvsValid;
// Signature terverifikasi dan hanya chain yang gagal; trust status
// melaporkannya sendiri
kCMSSignerInvalidCert:
Result:= pcvsValid;
kCMSSignerInvalidSignature, kCMSSignerUnsigned:
Result:= pcvsInvalid;
else
Result:= pcvsIndeterminate;
end;
end;
Baca kedua status tersebut sebagai ordered pair dan reporting logic langsung jelas. SignatureStatus = pcvsValid bersama TrustStatus = pcvsInvalid menggambarkan dokumen yang byte-nya utuh tetapi issuer-nya tidak dipercaya oleh Mac ini: anchor hilang dari Keychain, intermediate kedaluwarsa, atau chain tidak dapat dilengkapi secara offline. Itu adalah pertanyaan policy operator, bukan pertanyaan integritas dokumen, dan pembedaan tersebut tepat menjadi dasar sebagian besar kasus dalam catatan tentang mengapa validator menolak signature PAdES yang secara kriptografis sound
Di mana macOS sebenarnya memeriksa revocation?
Di dalam trust evaluation, itulah alasan TPdfCmsVerifyResult.RevocationStatus mengikuti TrustStatus dan bukan membawa verdict sendiri. SecPolicyCreateRevocation menghasilkan policy, policy tersebut bergabung dengan SecPolicyCreateBasicX509 dalam array yang diberikan kepada CMSDecoderCopySignerStatus, dan pekerjaan OCSP atau CRL terjadi ketika chain dibangun. Tidak ada jawaban terpisah yang dikembalikan, sehingga melaporkan satu jawaban berarti mengada-adakannya. Array itu sendiri membawa aturan ownership kecil yang layak disebut: CFArrayCreate me-retain kedua policy, sehingga dua local reference segera di-release setelahnya, sedangkan kasus single-policy melewati array sepenuhnya dan memberikan policy langsung, bentuk yang juga diterima API
Offline operation adalah flag eksplisit, bukan kebetulan konektivitas. Ketika TPdfCmsVerifyOptions.OnlineRetrieval bernilai False, backend menambahkan kSecRevocationNetworkAccessDisabled, membatasi evaluation pada response yang sudah di-cache di mesin, dan checkpoint callback tetap dipanggil dengan pcvstCryptographicSignature, pcvstChainBuild, dan pcvstRevocationCheck dalam urutan yang sama seperti yang dilaporkan Windows backend. Application code mengatur semua ini melalui higher-level options record
var
Options: TPadesTrustValidationOptions;
Report: TPadesValidationResult;
Stream: TFileStream;
begin
Options:= TPadesTrustValidationOptions.Default;
Options.CheckRevocation:= True;
Options.NetworkPolicy:= ptnpOffline; // hanya cached response
Options.CheckTimeStamps:= True;
Stream:= TFileStream.Create('contract.pdf', fmOpenRead or fmShareDenyWrite);
try
Report:= ValidatePadesTrust(Stream, Options);
finally
Stream.Free;
end;
if Report.SignatureCount= 0 then
Log('No signature dictionary in this document')
else if Report.Signatures[0].CmsSignatureStatus <> pcsValid then
Log('Document integrity failed')
else if Report.Signatures[0].CertificateTrustStatus <> pcsValid then
Log('Bytes intact, chain not trusted on this Mac');
end;
Get versus copy: release yang gagal di tempat lain
SecTrustGetCertificateAtIndex memiliki get semantics dan reference yang dikembalikannya tidak boleh di-release, sedangkan CMSDecoderCopySignerCert dan SecCertificateCopyData, yang berada beberapa baris di sebelahnya dalam routine yang sama, memiliki copy semantics dan wajib di-release. Core Foundation menyandikan seluruh aturan itu dalam satu kata kerja pada nama function, dan type system tidak menegakkan satu pun. Release borrowed reference tidak menimbulkan masalah di call site: trust object hanya menjadi unsound, lalu crash muncul belakangan di tempat yang tidak memiliki hubungan terlihat dengan certificate chain
ChainCount:= _SecTrustGetCertificateCount(Trust);
SetLength(Result.ChainCertificates, ChainCount);
for I:= 0 to ChainCount- 1 do
begin
// Get semantics: reference ini borrowed dan tidak di-release di sini
Cert:= _SecTrustGetCertificateAtIndex(Trust, I);
if Cert= nil then
Continue;
// Copy semantics: yang ini owned dan harus dikembalikan
CertData:= _SecCertificateCopyData(Cert);
if CertData= nil then
Continue;
try
Result.ChainCertificates[I]:= CFDataToBytes(CertData);
finally
_CFRelease(CertData);
end;
end;
Apa yang dijamin verifier ketika tidak ada backend yang menjawab?
Jawabannya unsupported, tidak pernah quiet pass. Ketika ConfigurePadesCmsVerifier tidak memasang apa pun dan platform default tidak dapat membantu, TPdfCmsVerifyResult kembali dengan setiap kolom diset unavailable dan PAdES validator memetakannya ke pcsUnsupported, sehingga build tanpa crypto backend melaporkan keadaan dengan jujur, bukan membuat klaim tentang signature. macOS binding juga konservatif ke arah yang sama: Security.framework dan CoreFoundation diakses melalui dlopen serta dlsym, sehingga framework yang tidak ada atau symbol name yang salah di binding ini muncul sebagai KeychainVerificationAvailable yang mengembalikan False dengan KeychainMissingSymbols menyebut pelakunya, bukan sebagai link failure dan bukan verdict yang salah. Ini adalah fail-closed posture yang sama yang digunakan component saat mencari native library, seperti dijelaskan dalam artikel tentang memuat native PDFium library pada target apa pun
Signature verification adalah bagian dari PDF stack tempat salah secara diam-diam lebih buruk daripada unavailable secara jelas, dan macOS memberi API yang cukup dermawan untuk membuat kedua hasil tersebut mudah dicapai. Gabungkan byte range dan terima copy-nya, simpan signature verdict serta chain verdict di kolom terpisah, hormati kata get dan copy, dan biarkan backend yang hilang menyatakan dirinya. Jika Anda memindahkan document workflow Delphi atau Free Pascal ke Mac dan membutuhkan PAdES signing serta validation di kedua sisi, PDFium Delphi Component mengirim Keychain backend berdampingan dengan backend Windows di balik satu interface