مقال تقني

التوقيع بـ PAdES بهوية macOS Keychain في Delphi

يوقّع PDFium VCL وثائق PAdES بمفتاح خاص محفوظ في macOS Keychain عبر خلفية تحل كل رمز من رموز Security وCoreFoundation وقت التشغيل بـ dlopen وdlsym. ولا شيء مرتبط وقت الربط، ما يعني أن اسم رمز مخطأً مطبعيًا يظهر بوصفه KeychainAvailable تعيد False وKeychainMissingSymbols تسمي الجاني، لا بوصفه خطأ رابط أو انهيارًا

وحُدِّر ذلك الاختيار بقيد غير مريح، وطريقة معالجته تتعمم. كُتبت الوحدة على آلة بلا macOS SDK، فكل اسم رمز إطار وكل ثابت جاء من التوثيق ولم يمكن التحقق من أي منها مقابل ترويسة. والاستجابة الخاطئة لذلك الموقف أن تكتب الكود بعناية وتأمل. أما الصائبة فأن ترتّب للأخطاء الحتمية أن تعلن عن نفسها بأقرب صورة يمكن تحديد موقعها

لماذا الربط الديناميكي هو القرار الصائب حتى على المنصة الهدف

لأنه يحوّل صنف إخفاق يوقف البرنامج إلى صنف إخفاق يبلّغ عن نفسه. فمرجع إطار مرتبط ساكنًا وهو خاطئ يخفق وقت الربط على الهدف ولا يُربط في أي مكان آخر. أما المرتبط ديناميكيًا وهو خاطئ فينتج خلفية غير متاحة وقائمة أسماء لم تُحل، وأول تشغيل على Mac يحوّل السؤال من لماذا هذا غير متاح إلى سطر واحد يسمي خطأ مطبعيًا

وهناك نافعة ثانية تدفع أجرها يوميًا لا مرة. ولأن الوحدة لا تربط إطارات، فهي تُصرَّف على كل منصة، فيواصل بناء Windows العادي فحص تركيبها وأنواعها وجملة uses فيها. فالوحدة التي لا تُصرَّف إلا على منصة لا يملكها أحد في الفريق وحدة بلا مترجم ينظر إليها، وتتعفن بصمت مع كل إعادة هيكلة لنوع مشترك

uses
  FPdfCrypto, FPdfCryptoMac;

var
  Options: TPadesSignerOptions;
begin
  if not KeychainAvailable then
    raise Exception.Create('Keychain backend unavailable, unresolved: ' +
      KeychainMissingSymbols);

  ConfigureKeychainSignerProvider;   // نُصّب بوصفه خلفية موقع PAdES
  ConfigureKeychainCmsVerifier;      // وبوصفه خلفية التحقق

  Writeln('signer backend  : ', PadesCryptoBackendName);
  Writeln('verify backend  : ', PadesCmsVerificationBackendName);

  Options := TPadesSignerOptions.Default;
  Options.CertificateThumbprint := 'B1 3F 9C ...';   // SHA-1، بأي حالة أحرف
  Options.PaddingScheme := psRsaPss;
end;

صنفان من الرموز المصدَّرة وطريقتان لقراءتهما

هذه هي التفصيلة الأكثر إرباكًا منفردة في الربط كله، وإلباشها بالمقلوب تُصرَّف بنظافة وتخفق وقت التشغيل. فـ CoreFoundation وSecurity تصدّران شيئين مختلفين في جوهرهما عبر استدعاء dlsym نفسه، وعلى الكود أن يعرف أيهما أي

الثوابت المسماة مثل مفاتيح صنف بنود السلسلة المفتاحية والمفردات المنطقية في CoreFoundation متغيرات مُصدَّرة محتواها هو CFStringRef أو CFBooleanRef الذي تريده. ويعيد dlsym عنوان ذلك المتغير، فعليك إلغاء المرجعية مرة واحدة للحصول على القيمة. أما بنى جدول الاستدعاء الرجعي مثل استدعاءات مفتاح القاموس وقيمته فبنى مُصدَّرة، ويعيد dlsym عنوان البنية، وهو بالضبط المؤشر الذي تتوقعه دالة إنشاء القاموس. وألغَ مرجعية تلك فسلّمت أول كلمة آلية في البنية وكأنها مؤشر

ولا خطأ منهما ينتج خطأ تصريف، ولا خطأ تشغيل واضحًا. تحصل على مؤشر نفاية يخفق في مكان ما في المصب. وطريقة جعل التمييز مستحيل الخطأ أن تتوقف عن الاعتماد على تذكره: دالتا مساعدة، واحدة تربط وتلغي المرجعية وواحدة تربط ولا تلغي، فيعلن موقع الاستدعاء أي صنف رمز يطلبها والمساعد يفرض الباقي

مخطط لخلفية macOS Keychain في PDFium VCL تحل رموز Security وCoreFoundation عبر dlsym: kSecClass متغير مُصدَّر تلغي BindConstant مرجعيته مرة واحدة للحصول على قيمة CFStringRef، بينما kCFTypeDictionaryKeyCallBacks بنية مُصدَّرة تمررها BindStruct بالعنوان، وخلط القاعدتين ينتج مؤشرات نفاية في المصب
استدعاء dlsym واحد يعيد شيئين مختلفين في الجوهر: عنوان متغير يحمل CFTypeRef وعنوان بنية استدعاء رجعي. ودالتا مساعدة تتخذان قرار إلغاء المرجعية أو لا في موقع الربط بدل الذاكرة
// متغير مُصدَّر: dlsym يعطي عنوان متغير يحمل الـ
// CFTypeRef، فألغِ المرجعية مرة واحدة
FSecClassKey := BindConstant(SecurityLib, 'kSecClass');

// بنية مُصدَّرة: dlsym يعطي عنوان البنية نفسه، وهو
// ما تريده الواجهة. لا تلغِ المرجعية
FKeyCallbacks := BindStruct(CoreFoundationLib,
  'kCFTypeDictionaryKeyCallBacks');

لماذا يحتاج توقيع RSA-PSS بديلين احتياطيين منفصلين

لأن الخوارزمية يمكن أن تكون غائبة بطريقتين مستقلتين، وواحدة منهما فقط سؤال إصدار. فثابت خوارزمية توقيع ملخص PSS ظهر في macOS 10.13، فعلى نظام أقدم الرمز ببساطة غير موجود ويحصل الربط على nil. وهذا هو فحص الإصدار. ومنفصلًا، على نظام موجود فيه الثابت، قد يرفض مفتاح بعينه ذلك رغم ذلك، وتجيب إطار عن ذلك السؤال عبر SecKeyIsAlgorithmSupported لذلك المفتاح. فمفتاح مدعوم عتاديًا أو مفتاح بسمات مقيِّدة يستطيع أن يعتذر عن PSS بينما مفتاح برمجي على الآلة نفسها يقبله

وعلى كلا المسارين أن يؤديا إلى البديل الاحتياطي نفسه: التحول إلى PKCS#1 v1.5. والجزء الحرج أن على البديل أن يغيّر معرف الخوارزمية المكتوب في بنية CMS كذلك، لا استدعاء التوقيع وحده. فإصدار معرف خوارزمية PSS مع إنتاج توقيع v1.5 فعلًا ينتج وثيقة يرفضها كل مدقق رفضًا قاطعًا، وهذا أسوأ قطعًا من التبليغ بأن PSS غير مدعومة. فالهبوط بمستوى مقبول، وعدم التطابق بين ما تعلنه وما فعلته ليس مقبولًا، وهذه قاعدة عامة لكود التوقيع لا غرابة macOS. وتفصيلات مستوى التوقيع معروضة في توقيع ملفات PDF بـ PAdES B-B

سلسلة قرار تبين لماذا يحتاج توقيع RSA-PSS في خلفية Keychain في PDFium VCL بديلين احتياطيين مستقلين: يعيد dlsym nil لثابت توقيع الملخص على إصدارات macOS قبل 10.13، ويستطيع SecKeyIsAlgorithmSupported رفض مفتاح مدعوم عتاديًا، ويصب كلا البوابتين في الهبوط نفسه إلى PKCS#1 v1.5 الذي يجب أن يتغير معه معرف خوارزمية CMS
قد يكون PSS غير متاح مرتين، مرة لكل إصدار macOS ومرة لكل مفتاح، وبوابة الإصدار وحدها سؤال نظام. ويصب كلا البوابتين في الهبوط نفسه إلى v1.5، ويتلو معرف CMS ذلك

ترميز توقيع ECDSA وانقلاب يستحق الملاحظة

مسار المنحنى الإهليلجي لا يحتاج أي تحويل على macOS، وهذا عكس ما يتطلبه ربط PKCS#11. فخوارزمية توقيع الملخص في إطار Security لـ ECDSA تعيد التوقيع وهو أصلًا بصيغة X9.62 DER، وهذا بالضبط ما يريده CMS. أما رمز PKCS#11 فيعيد زوج P1363 الخام ذا العرض الثابت، وعلى إعادة ترميزه قبل أن يدخل بنية توقيع

فخلفيتان تنفذان الواجهة نفسها تحتاجان معاملة متعاكسة للخوارزمية نفسها، ولا واحدة منهما مخطئة. وهذا بالضبط صنف الفرق الذي يجب على التجريد أن يمتصه لا يكشفه: طبقة PAdES تطلب من مزود أن يوقّع، وتبقى اصطلاحات الترميز داخل المزود. فإن سربت إلى الأعلى، انتهى كل مستدعي حاملًا شرطًا لكل خلفية. ويظهر الشكل نفسه في قصة التوقيع البعيد الموصوفة في جلسات توقيع PAdES البعيدة مقابل HSM

مقارنة ترميز توقيع ECDSA عبر خلفيتين من موقع PAdES في PDFium VCL: يعيد إطار Security في macOS Keychain قيمة X9.62 DER يقبلها CMS بلا أي تحويل، بينما يعيد رمز PKCS#11 زوج P1363 الخام ذا العرض الثابت الذي يجب إعادة ترميزه، لذا يبقي ResolvePadesSigner اصطلاحات الترميز داخل المزود
واجهة ECDSA نفسها تحتاج معاملة متعاكسة لكل خلفية: يسلم Security قيمة DER تامة بينما يسلم رمز PKCS#11 قيمة P1363 خام، فيسكن التحويل داخل المزود ولا يرى المستدعون شرطًا لكل خلفية أبدًا
// واجهة المزود نفسها على كل منصة، فيكون الاختيار قرار بدء
// لا قرار كل استدعاء
{$IFDEF DARWIN}
  if KeychainAvailable then
    ConfigureKeychainSignerProvider;
{$ENDIF}
{$IFDEF MSWINDOWS}
  // مزود Windows CNG ينصبه وحدة المنصة
{$ENDIF}

if not PadesCryptoAvailable then
  raise Exception.Create('no signing backend on this platform');

// من هنا كود التوقيع محايد المنصات
Signer := ResolvePadesSigner(Options);

قواعد عدّ المراجع التي تجلس بينها ثلاثة أسطر

إدارة ذاكرة Core Foundation تتبع اصطلاحات تسمية، والفخ هنا أن دوالًا باصطلاحات مختلفة تظهر جنبًا إلى جنب في الكتلة القصيرة نفسها. فدالة تحصل على شهادة من كائن ثقة تعيد مرجعًا مستعارًا يجب ألا يُحرَّر. والدوال التي تنسخ شهادة موقع أو تنسخ بياناتها تعيد مراجع مملوكة يجب تحريرها. ثلاثة استدعاءات متتالية، وقاعدتا ملكية، وتحرير المستعار لا يخفق عند ذلك السطر، بل يفسد عدد احتجاز ويأسر شيئًا غير ذي صلة لاحقًا

والتخفيف أن تقرأ الفعل في اسم كل دالة إطار قبل أن تكتب التنظيف، في كل مرة، بلا استثناء. إنه المقابل CoreFoundation للتحقق هل تعيد واجهة برمجية نسخة أم منظر، وكلفة الخطأ انهيار متقطع لا خطأ

ما لا تدعي به هذه الخلفية

لم تجرُ هذه الخلفية على macOS وقت كتابة هذه السطور، وقول ذلك بوضوح أنفع من ضمانة ضمنية. وما هو مثبت الصواب أضيق وما زال نفيسًا: الوحدة تُصرَّف على Windows ضمن البناء اليومي، وكل رمز إطار يُربط بالاسم وقت التشغيل مع تعداد الإخفاقات، ومنطق اختيار الخوارزمية شاملًا البديلين الاحتياطيين لـ PSS هو Pascal عادي يمكن مراجعته واستدلاله. وأول تشغيل على Mac سينجح أو سينتج قائمة أسماء للإصلاح

والنظير التحققي، الذي يستخدم محلل CMS الأعلى مستوى بدل تركيب بنية CMS يدويًا، مغطى في التحقق من تواقيع PDF على macOS بـ SecTrust، وهو يتشارك بنية الربط نفسها والنهج التشخيصي نفسه

والفكرة القابلة للنقل هنا عن موضع الخطر لا عن macOS. حين يجب عليك أن تكتب كودًا مقابل واجهة لا تستطيع التحقق منها، اختر البناء الذي تكون الأخطاء فيه أرخص تحديدًا لموقعها. فالربط الديناميكي بقائمة صريحة للأسماء غير المحلولة يحوّل عشرين افتراضًا لا يمكن التحقق منها إلى سطر تشخيصي واحد. وكلا الخلفيتين تردان شيفرة مصدرية مع مكوّن PDFium Delphi، فإذا احتاج اسم رمز إلى تصحيح فسيكون تغيير سطر واحد في شجرتك أنت لا تذكرة دعم