PDFium Delphi Component ตรวจ PDF signature บน macOS ผ่าน TPdfKeychainCmsVerifier ซึ่งเป็น CMS verification backend ที่สร้างบน Apple CMSDecoder กับ SecTrust แทนการ parse CMS ด้วยมือ ConfigureKeychainCmsVerifier ติดตั้ง backend นี้ และ call เดียวของ CMSDecoderCopySignerStatus จะคืน signature verdict, SecTrust handle และ certificate result code ซึ่งตรงกับ column pair ที่ TPdfCmsVerifyResult มีอยู่แล้วบน Windows
สถานการณ์ที่บังคับให้ทำงานนี้ธรรมดาและพบได้บ่อย Lazarus build ของ document archive รันบน Mac เปิด signed contract แล้ว signature ทุกตัวกลับมาเป็น pcsUnsupported ไฟล์ไม่ได้มีปัญหาอะไร เพียงแต่ signature verification ไม่มี backend นอก Windows และ PAdES validator ไม่ยอมเดาเมื่อไม่มี backend version 3.111.0 ของ PDFiumPas เปิด seam ด้วย IPdfCmsVerifier และ ConfigurePadesCmsVerifier ส่วน version 3.113.0 เติม backend ให้ macOS ส่วนที่น่าสนใจของ port นี้ไม่ใช่ plumbing แต่คือสามจุดที่ Apple API มีรูปทรงไม่เหมือน Windows
ทำไม PDF signature จึงครอบคลุมสอง byte range
เพราะ signature ไม่สามารถครอบ byte ที่เก็บตัวมันเอง ISO 32000-1 §12.8.1 วาง CMS SignedData blob ไว้ใน string /Contents ของ signature dictionary และบอก extent ที่ signed ด้วย /ByteRange ซึ่งเป็นคู่ offset กับ length ที่ครอบทุกอย่างทั้งสองข้างของช่องว่างนั้น ทุก platform จึงมีสอง segment และมีช่องว่างหนึ่งช่องตรงกลาง
platform ต่างกันตรงวิธีส่ง segment เหล่านี้เข้า crypto layer และความต่างทำให้ memory เพิ่ม บน Windows CryptVerifyDetachedMessageSignature รับ array ของ pointer กับ length ดังนั้น span สองช่วงส่งเข้าไปตามที่อยู่ใน buffer ได้เลยและไม่ต้อง duplicate แต่ Apple CMSDecoderSetDetachedContent รับ CFData เดียวและไม่มีรูปแบบหลาย segment backend ของ macOS จึง concatenate range สองช่วงเข้า contiguous buffer ก่อน decode นี่คือการ copy signed byte เต็มชุดเป็นครั้งที่สอง บน scanned archive ขนาด 400 MB มันคือ memory peak จริง ขนาดโตตาม document ไม่ใช่ตาม signature และไม่มี API อื่นให้เลือก จัดขนาด batch worker ให้ดีแทนการค้นพบเรื่องนี้บนเครื่องลูกค้า
call เดียวเติมสอง column ของ TPdfCmsVerifyResult
CMSDecoderCopySignerStatus ใจกว้างผิดปกติสำหรับ Security.framework entry point เพราะ call เดียวคืน signer status, SecTrustRef สำหรับ chain ที่มันสร้าง และ OSStatus สำหรับ certificate evaluation ทั้งสามค่าลงตรงใน record ที่ PAdES validator ใช้อยู่แล้ว signer status กลายเป็น SignatureStatus certificate result กลายเป็น TrustStatus และ raw value ถูกเก็บไว้ใน SignatureError กับ TrustError เพื่อให้ support ticket อ้างหมายเลขแทนคำคุณศัพท์ caller ไม่ต้องแตะ IPdfCmsVerifier เอง ValidatePadesCompliance กับ ValidatePadesTrust จะ route verification ทุกครั้งผ่าน backend ที่ติดตั้ง ดังนั้น code ที่อ่าน TPadesSignatureValidation จึงเหมือนกันแบบ byte ต่อ byte บนทั้งสอง platform ตามที่อธิบายใน walkthrough เรื่อง การตรวจ signature dictionary และ PAdES level ของ PDF ใน Delphi
uses
FPdfCrypto, FPdfCryptoMac, FPdfPades;
procedure InstallMacVerifier;
begin
// symbol สำหรับ signing กับ verification มาจาก framework คนละชุด
// จึงมีตัวหนึ่งอยู่ในขณะที่อีกตัวหายไปได้
if not KeychainVerificationAvailable then
raise Exception.CreateFmt('Security.framework symbols missing: %s',
[KeychainMissingSymbols]);
ConfigureKeychainCmsVerifier;
// PadesCmsVerificationBackendName ตอนนี้ตอบ 'macOS Security.framework'
if not PadesCmsVerificationAvailable then
raise Exception.Create('No CMS verification backend is installed');
end;
ทำไม kCMSSignerInvalidCert จึงรายงาน signature ที่ valid
เพราะ Apple ให้ความหมายกับค่านี้แคบกว่าชื่อของมัน signature เอง verify ผ่านแล้ว มีเพียง certificate chain ที่สร้างไม่ได้ TPdfKeychainCmsVerifier จึง map kCMSSignerInvalidCert เป็น pcvsValid ใน column SignatureStatus และปล่อยให้ปัญหา certificate ไปโผล่ใน TrustStatus ซึ่งเป็นที่ของ chain problem หากรวมมันเข้า signature verdict component จะบอก operator ว่า document ที่ไม่ถูกแก้ไขถูกแก้ไขแล้ว ซึ่งเป็น false alarm ที่แย่ที่สุดที่ signature validator สร้างได้
function MapSignerStatus(Status: LongWord): TPdfCmsVerifyStatus;
begin
case Status of
kCMSSignerValid:
Result:= pcvsValid;
// signature verify แล้ว มีเพียง chain ที่ไม่ผ่าน trust
// และ TrustStatus จะรายงานแยกเอง
kCMSSignerInvalidCert:
Result:= pcvsValid;
kCMSSignerInvalidSignature, kCMSSignerUnsigned:
Result:= pcvsInvalid;
else
Result:= pcvsIndeterminate;
end;
end;
อ่าน status สองตัวเป็น ordered pair แล้ว reporting logic จะตรงไปตรงมา SignatureStatus = pcvsValid พร้อม TrustStatus = pcvsInvalid อธิบาย document ที่ byte ยังสมบูรณ์แต่ issuer ของมันไม่เป็นที่เชื่อถือบน Mac เครื่องนี้ อาจเป็น anchor ที่หายจาก Keychain, intermediate ที่หมดอายุ หรือ chain ที่ต่อให้ครบแบบ offline ไม่ได้ นี่เป็นคำถามด้าน operator policy ไม่ใช่คำถามด้าน document integrity และความแตกต่างนี้คือแกนของกรณีส่วนใหญ่ในบันทึกเรื่อง เหตุผลที่ validator reject PAdES signature ซึ่งมีความถูกต้องทาง cryptography
macOS ตรวจ revocation ที่ไหนจริง
อยู่ภายใน trust evaluation นั่นคือเหตุผลที่ TPdfCmsVerifyResult.RevocationStatus ตามหลัง TrustStatus แทนการมี verdict ของตัวเอง SecPolicyCreateRevocation สร้าง policy แล้ว policy นั้น join กับ SecPolicyCreateBasicX509 ใน array ที่ส่งเข้า CMSDecoderCopySignerStatus ส่วน OCSP หรือ CRL work เกิดขึ้นตอน chain ถูกสร้าง ไม่มี answer แยกกลับมา ดังนั้นการรายงานอีกค่าหนึ่งจะเท่ากับการแต่งขึ้นเอง array ยังมี ownership rule เล็ก ๆ ที่ควรเรียกชื่อ CFArrayCreate retain policy ทั้งสองตัว ดังนั้น local reference สองตัวจะถูก release ได้ทันที ส่วนกรณีมี policy เดียวจะข้าม array และส่ง policy เข้า API โดยตรง ซึ่ง API รองรับรูปแบบนี้เช่นกัน
offline operation เป็น flag ที่ระบุอย่างชัดเจน ไม่ใช่ผลจาก connectivity ที่บังเอิญหาย เมื่อ TPdfCmsVerifyOptions.OnlineRetrieval เป็น False backend จะเพิ่ม kSecRevocationNetworkAccessDisabled เพื่อจำกัด evaluation ให้ใช้ response ที่ cache อยู่ในเครื่องเท่านั้น และ checkpoint callback ยังคง fire pcvstCryptographicSignature, pcvstChainBuild และ pcvstRevocationCheck ตามลำดับเดียวกับที่ Windows backend รายงาน application code ตั้งค่าทั้งหมดนี้ผ่าน options record ระดับสูง
var
Options: TPadesTrustValidationOptions;
Report: TPadesValidationResult;
Stream: TFileStream;
begin
Options:= TPadesTrustValidationOptions.Default;
Options.CheckRevocation:= True;
Options.NetworkPolicy:= ptnpOffline; // ใช้ 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 กับ copy: release ที่ไปพังที่อื่น
SecTrustGetCertificateAtIndex มี get semantics และ reference ที่คืนมาต้องไม่ถูก release ส่วน CMSDecoderCopySignerCert กับ SecCertificateCopyData ที่อยู่ห่างกันเพียงไม่กี่บรรทัดใน routine เดียวกันมี copy semantics และต้อง release Core Foundation encode กฎทั้งหมดไว้ใน verb หนึ่งคำของชื่อ function แต่ type system ไม่ enforce เลย หาก release borrowed reference จะไม่เกิดอะไรตรง call site trust object แค่กลายเป็นสิ่งที่ไม่ sound และ crash จะมาถึงภายหลังในจุดที่ไม่เห็นความเชื่อมโยงกับ certificate chain
ChainCount:= _SecTrustGetCertificateCount(Trust);
SetLength(Result.ChainCertificates, ChainCount);
for I:= 0 to ChainCount- 1 do
begin
// Get semantics: reference นี้ถูกยืมมาและห้าม release ตรงนี้
Cert:= _SecTrustGetCertificateAtIndex(Trust, I);
if Cert= nil then
Continue;
// Copy semantics: ค่านี้เป็นของเราและต้องคืนกลับ
CertData:= _SecCertificateCopyData(Cert);
if CertData= nil then
Continue;
try
Result.ChainCertificates[I]:= CFDataToBytes(CertData);
finally
_CFRelease(CertData);
end;
end;
เมื่อไม่มี backend ตอบ verifier รับประกันอะไร
มันรับประกันคำตอบว่า unsupported ไม่ใช่ quiet pass หาก ConfigurePadesCmsVerifier ยังไม่ได้ติดตั้งอะไรและ platform default ก็ช่วยไม่ได้ TPdfCmsVerifyResult จะกลับมาพร้อมทุก column เป็น unavailable และ PAdES validator จะ map เป็น pcsUnsupported ดังนั้น build ที่ไม่มี crypto backend จะรายงานอย่างซื่อตรงแทนการอ้างอะไรเกี่ยวกับ signature macOS binding ก็ conservative ไปในทิศทางเดียวกัน Security.framework กับ CoreFoundation ถูกเรียกผ่าน dlopen และ dlsym ดังนั้น framework ที่หายไปหรือ symbol name ที่ binding เขียนผิดจะโผล่เป็น KeychainVerificationAvailable คืน False พร้อม KeychainMissingSymbols ที่ระบุผู้ร้าย ไม่ใช่ link failure และไม่ใช่ wrong verdict นี่คือ fail-closed posture เดียวกับตอน component หา native library ตามที่อธิบายใน การโหลด PDFium native library บน target ใด ๆ
signature verification เป็นส่วนของ PDF stack ที่การผิดแล้วเงียบแย่กว่าการ unavailable แล้วดัง และ macOS ให้ API ที่เอื้อให้เกิดทั้งสองแบบง่ายมาก concatenate byte range แล้วรับต้นทุน copy แยก signature verdict กับ chain verdict เป็นคนละ column เคารพ verb get กับ copy และปล่อยให้ backend ที่หายไปรายงานตัวเอง หากคุณกำลังย้าย Delphi หรือ Free Pascal document workflow ไปยัง Mac และต้องการ PAdES signing กับ validation ทั้งสองฝั่ง PDFium Delphi Component มี Keychain backend อยู่ข้าง Windows backend ภายใต้ interface เดียวกัน