يتحقق PDFium Delphi Component من توقيعات PDF على macOS عبر TPdfKeychainCmsVerifier، وهي backend للتحقق من CMS مبنية على Apple CMSDecoder وSecTrust بدل تحليل CMS يدويًا. وتثبتها ConfigureKeychainCmsVerifier، بينما يعيد استدعاء واحد لـCMSDecoderCopySignerStatus حكم التوقيع ومقبض SecTrust ورمز نتيجة الشهادة، أي الزوج نفسه من الأعمدة الذي يحمله TPdfCmsVerifyResult أصلًا على Windows
السيناريو الذي فرض العمل ممل ومألوف. يعمل بناء Lazarus لأرشيف مستندات على Mac، ويفتح عقدًا موقعًا، وتعود كل توقيعاته بالقيمة pcsUnsupported. لا شيء خاطئ في الملف. لكن التحقق من التوقيع لم يكن يملك backend خارج Windows، ورفض validator الخاص بـPAdES التخمين في غيابها. فتح الإصدار 3.111.0 من PDFiumPas الوصلة عبر IPdfCmsVerifier وConfigurePadesCmsVerifier، ثم ملأها الإصدار 3.113.0 على macOS. والجزء المثير في ذلك النقل ليس plumbing، بل المواضع الثلاثة التي لا تملك فيها API الخاصة بـApple الشكل نفسه الذي تملكه API الخاصة بـWindows
لماذا يغطي توقيع PDF نطاقي بايتات؟
لأن التوقيع لا يستطيع تغطية البايتات التي تحمله. تضع ISO 32000-1 §12.8.1 كائن CMS SignedData داخل سلسلة /Contents في قاموس التوقيع، وتصف الامتداد الموقع باستخدام /ByteRange، وهي مجموعة من أزواج الإزاحة والطول التي تغطي كل شيء على جانبي تلك الفجوة. مقطعان وفجوة في الوسط على كل منصة
وتختلف المنصات في كيفية وصول هذه المقاطع إلى طبقة التشفير، ويكلف الاختلاف ذاكرة. فعلى Windows تقبل CryptVerifyDetachedMessageSignature مصفوفة من المؤشرات والأطوال، ولذلك يدخل الامتدادان كما هما في المخزن ولا يحدث نسخ. أما Apple CMSDecoderSetDetachedContent فتقبل CFData واحدًا ولا تملك صيغة متعددة المقاطع، ولذلك تجمع backend الخاصة بـmacOS النطاقين في مخزن متصل قبل فكهما. وهذا نسخ ثانٍ كامل من البايتات الموقعة. وفي أرشيف ممسوح حجمه 400 MB تبلغ هذه ذروة ذاكرة حقيقية، وتكبر مع المستند لا مع التوقيع، ولا توجد API بديلة تلجأ إليها. اضبط حجم worker الدفعي وفق ذلك بدل اكتشافه على جهاز عميل
استدعاء واحد يملأ عمودين من TPdfCmsVerifyResult
إن CMSDecoderCopySignerStatus سخية على نحو غير معتاد بالنسبة إلى نقطة دخول في Security.framework: فاستدعاء واحد يعيد حالة الموقع، وSecTrustRef للسلسلة التي بناها، وOSStatus لتقييم الشهادة. وتهبط هذه القيم مباشرة في السجل الذي يستهلكه validator الخاص بـPAdES أصلًا، فتصبح حالة الموقع SignatureStatus، ونتيجة الشهادة TrustStatus، وتحفظ القيم الخام في SignatureError وTrustError حتى يستطيع ticket الدعم اقتباس رقم بدل صفة. ولا يلمس المستدعون IPdfCmsVerifier بأنفسهم أبدًا؛ إذ توجه ValidatePadesCompliance وValidatePadesTrust كل تحقق عبر backend المثبتة، ولذلك يكون الكود الذي يقرأ TPadesSignatureValidation مطابقًا بايتًا ببايت على المنصتين، كما يوضح المسار في فحص قواميس توقيع PDF ومستويات PAdES في Delphi
uses
FPdfCrypto, FPdfCryptoMac, FPdfPades;
procedure InstallMacVerifier;
begin
// تحل عمليات التوقيع والتحقق رموز 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 عن توقيع صالح؟
لأن Apple تمنح تلك القيمة معنى أضيق مما يوحي به اسمها: فقد تم التحقق من التوقيع نفسه، وتعذر فقط بناء سلسلة الشهادة. ولذلك تربط TPdfKeychainCmsVerifier القيمة kCMSSignerInvalidCert بـpcvsValid في عمود SignatureStatus، وتترك مشكلة الشهادة تظهر عبر TrustStatus، حيث تنتمي مشكلة السلسلة. ودمجها في حكم التوقيع سيجعل المكوّن يخبر المشغّل بأن مستندًا لم يُعبث به قد عُدّل، وهو أسوأ إنذار كاذب يمكن أن يطلقه validator للتوقيع
function MapSignerStatus(Status: LongWord): TPdfCmsVerifyStatus;
begin
case Status of
kCMSSignerValid:
Result:= pcvsValid;
// تم التحقق من التوقيع ولم تفشل إلا السلسلة، وهو ما يبلغه
// status الثقة وحده
kCMSSignerInvalidCert:
Result:= pcvsValid;
kCMSSignerInvalidSignature, kCMSSignerUnsigned:
Result:= pcvsInvalid;
else
Result:= pcvsIndeterminate;
end;
end;
اقرأ الحالتين كزوج مرتب، وستكتب منطق التقارير نفسه. فـSignatureStatus = pcvsValid مع TrustStatus = pcvsInvalid يصف مستندًا بايتاته سليمة، لكن Mac هذا لا يثق بمصدره: قد يكون anchor غائبًا من Keychain، أو intermediate منتهيًا، أو سلسلة لا يمكن إكمالها دون اتصال. وهذا سؤال سياسة للمشغّل لا سؤال سلامة مستند، والتمييز هو نفسه وراء معظم الحالات في ملاحظة سبب رفض validator لتوقيعات PAdES السليمة تشفيريًا
أين يتحقق macOS من الإلغاء فعلًا؟
داخل تقييم الثقة، ولهذا يتبع TPdfCmsVerifyResult.RevocationStatus قيمة TrustStatus بدل حمل حكم خاص به. تنتج SecPolicyCreateRevocation سياسة، وتنضم هذه السياسة إلى SecPolicyCreateBasicX509 في المصفوفة الممررة إلى CMSDecoderCopySignerStatus، ويحدث عمل OCSP أو CRL حيث تُبنى السلسلة. ولا تعود إجابة منفصلة، ولذلك فإن الإبلاغ عنها يعني اختراعها. وتحمل المصفوفة نفسها قاعدة ملكية صغيرة تستحق التسمية: إذ تحتفظ CFArrayCreate بالسياستين، ولذلك تُحرر الإشارتان المحليتان فورًا بعد ذلك، بينما تتجاوز حالة السياسة الواحدة المصفوفة بالكامل وتمرر السياسة مباشرة، وهي صيغة تقبلها API أيضًا
العمل دون اتصال علم صريح لا مصادفة اتصال. فعندما تكون قيمة TPdfCmsVerifyOptions.OnlineRetrieval هي False، تضيف backend القيمة kSecRevocationNetworkAccessDisabled وتحصر التقييم في الاستجابات المخزنة مؤقتًا على الجهاز، كما يستمر callback نقطة التحقق في إطلاق pcvstCryptographicSignature وpcvstChainBuild وpcvstRevocationCheck بالترتيب نفسه الذي يبلغه backend الخاص بـWindows. وتضبط شيفرة التطبيق كل ذلك عبر سجل الخيارات الأعلى مستوى
var
Options: TPadesTrustValidationOptions;
Report: TPadesValidationResult;
Stream: TFileStream;
begin
Options:= TPadesTrustValidationOptions.Default;
Options.CheckRevocation:= True;
Options.NetworkPolicy:= ptnpOffline; // الاستجابات المخزنة مؤقتًا فقط
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: الإصدار الذي يفشل في مكان آخر
تملك SecTrustGetCertificateAtIndex دلالات get، ولا يجوز تحرير المرجع الذي تعيده أبدًا، بينما تملك CMSDecoderCopySignerCert وSecCertificateCopyData، الموجودتان على بعد بضعة أسطر في الدالة نفسها، دلالات copy ويجب تحريرهما. وترمز Core Foundation إلى القاعدة كلها في فعل واحد داخل اسم الدالة ولا يفرض نظام الأنواع أي جزء منها. حرر المرجع المستعار، ولن يحدث شيء خاطئ عند موضع الاستدعاء: سيصبح كائن الثقة غير سليم فحسب، وسيصل crash لاحقًا في موضع لا علاقة ظاهرة له بسلاسل الشهادات
ChainCount:= _SecTrustGetCertificateCount(Trust);
SetLength(Result.ChainCertificates, ChainCount);
for I:= 0 to ChainCount- 1 do
begin
// دلالات Get: هذا المرجع مستعار ولا يُحرر هنا
Cert:= _SecTrustGetCertificateAtIndex(Trust, I);
if Cert= nil then
Continue;
// دلالات Copy: هذا المرجع مملوك ويجب إعادته
CertData:= _SecCertificateCopyData(Cert);
if CertData= nil then
Continue;
try
Result.ChainCertificates[I]:= CFDataToBytes(CertData);
finally
_CFRelease(CertData);
end;
end;
ما الذي يضمنه المدقق عندما لا تجيب أي backend؟
يضمن أن الإجابة غير مدعومة، ولا يضمن نجاحًا صامتًا أبدًا. فعندما لا تثبت ConfigurePadesCmsVerifier شيئًا ولا يستطيع افتراضي المنصة المساعدة، يعود TPdfCmsVerifyResult وكل أعمدته مضبوطة على unavailable، ويحوّل validator الخاص بـPAdES ذلك إلى pcsUnsupported، ولذلك يبلغ البناء الذي لا يملك backend تشفير بصدق بدل الادعاء بأي شيء عن التوقيع. كما أن ربط macOS محافظ عمدًا في الاتجاه نفسه: إذ يُوصل إلى Security.framework وCoreFoundation عبر dlopen وdlsym، ولذلك يظهر غياب framework أو اسم رمز أخطأ هذا الربط فيه على شكل إعادة KeychainVerificationAvailable للقيمة False مع تسمية KeychainMissingSymbols للمخطئ، لا على شكل فشل ربط ولا حكم خاطئ. وهذا هو موقف الفشل الآمن نفسه الذي يتبناه المكوّن عند بحثه عن المكتبة الأصلية، كما يصفه موضوع تحميل مكتبة PDFium الأصلية على أي هدف
التحقق من التوقيع هو الجزء من مكدس PDF الذي يكون الخطأ الصامت فيه أسوأ من عدم التوفر الصريح، ويمدك macOS بـAPI سخية بما يكفي لجعل الوصول إلى النتيجتين سهلًا. اضمم نطاقات البايت واقبل النسخ، وأبق حكم التوقيع وحكم السلسلة في عمودين منفصلين، واحترم فعلي get وcopy، ودع backend الغائبة تقول ذلك. وإذا كنت تنقل مسار مستندات Delphi أو Free Pascal إلى Mac وتحتاج توقيع PAdES والتحقق منه على الجانبين، فإن PDFium Delphi Component يشحن backend Keychain إلى جانب Backend Windows خلف واجهة واحدة