مقال تقني

خلفية طوابع زمنية عبر libcurl لـ PDFium VCL على FPC

يرسل PDFium VCL طلبات طوابع زمنية RFC 3161 عبر libcurl على الأهداف غير الوندوزية، مرتبطًا ديناميكيًا بثمانية رموز، على صورة خلفية Windows التي ترتبط بـ WinHTTP. وإعددا خيارات يقرران هل النقل موثوق تحت الحمل، ووُلِّدت الوحدة كلها على آلة لا تستطيع تصريفها لمنصتها الهدف

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

لماذا libcurl بدل عميل HTTP في FPC

لأن البديل يسحب رصة TLS إلى المستودع ثم يجعلك تصون كشف إصدارها. فالطريق البديهي على Free Pascal هو fphttpclient بطبقة مقابس OpenSSL، وهو يخفق في التفاصيل: روابط OpenSSL في FPC 3.2.2 تكشف OpenSSL 3.x بصورة غير موثوقة على معظم التوزيعات الحالية، وmacOS تضيف فروق LibreSSL فوق ذلك. وما يبدأ استدعاء HTTP صغيرًا يصير صيانة مستمرة لـ ABI TLS لشخص آخر

وlibcurl تحل خلفية TLS الخاصة بها وتتحقق من السلاسل مقابل مخزن الثقة في المنصة، فالجانب Pascal لا يحتاج شيئًا من ذلك. وطبقة الربط ثمانية رموز. وهذا العدد هو الحجة: سطح أصغر بين كودك واعتمادية متحركة يعني مواضع أقل يكسر فيها ترقية توزيع عملك، وهو يطابق خلفية Windows القائمة التي تربط بضع نقاط دخول WinHTTP بالطريقة نفسها

uses
  FPdfTsaFpc;

var
  ReqDer, RespDer: TBytes;
begin
  if not TsaHttpAvailable then
    raise Exception.Create('no HTTP transport for timestamping');

  Writeln('TSA transport: ', TsaHttpBackendName);

  ReqDer := BuildTimeStampQuery(DocumentDigest);
  if PostTimeStampQuery('https://tsa.example.org/tsr', ReqDer, RespDer) then
    AttachTimeStampToken(RespDer)
  else
    raise Exception.Create('timestamp request failed');
end;

إعلان دالة C متغيرة الوسائط في Pascal

فـ curl_easy_setopt وcurl_easy_getinfo متغيرتا الوسائط على الجانب C، ولا سبيل لـ Object Pascal إلى التعبير عن ذلك. والنهج الذي يعمل إعلان عدة نماذج ثابتة، واحد لكل صنف وسائط، كلها تشير إلى الرمز المُصدَّر نفسه: نسخة آخذة بـ long، ونسخة آخذة بمؤشر، وهكذا، تُختار في موقع الاستدعاء بما تمرره فعلًا

وهذا آمن لسبب محدد يستحق الفهم لا النسخ. فكل نوع من أنواع الوسائط تلك يُمرَّر في سجل عدد صحيح تحت اصطلاحات الاستدعاء المنصاية، وهو بالضبط حيث تقرأه va_arg في تنفيذ C. فالحيلة إذن تسري على الأعداد الصحيحة والمؤشرات والمقبوضات، ولا تسري على وسائط النقاط العائمة التي تسافر في سجلات مختلفة. لا تضف نسخة آخذة بـ double على افتراض أن النمط يتعمم

// رمز مُصدَّر واحد وعدة نماذج ثابتة. كل نسخة تمرر وسيطها في
// سجل عدد صحيح، وهو حيث يقرأه الجانب C. نسخة نقاط عائمة
// لن تعمل ويحظر إضافتها
type
  TCurlSetOptLong = function(Handle: Pointer; Option: Integer;
    Value: NativeInt): Integer; cdecl;
  TCurlSetOptPtr  = function(Handle: Pointer; Option: Integer;
    Value: Pointer): Integer; cdecl;

var
  curl_easy_setopt_long: TCurlSetOptLong;
  curl_easy_setopt_ptr:  TCurlSetOptPtr;

إعددان يقرران هل يكتمل الطلب

الأول ترويسة Expect: فارغة صريحة. يفعّل libcurl مصافحة HTTP 100-continue لأجسام الطلبات فوق نحو كيلوبايت، واستعلام طابع زمني بطلب شهادة يجتاز تلك العتبة عادة. وبعض خوادم TSA لا تجيب أبدًا عن المتابعة، فينتظر العميل مهلة كاملة قبل أن يرسل جسمًا كان الخادم سيقبله فورًا. وإرسال ترويسة Expect: فارغة يكبت المصافحة، فيمر الطلب في دورة ذهاب وإياب واحدة

والثاني CURLOPT_NOSIGNAL الذي يجب ضبطه. فبدونه ينفذ libcurl مهلة حل الأسماء باستخدام SIGALRM، وتلك الآلية ليست آمنة الخيوط. والتوقيع يجري على خيط عامل، فالسلوك الافتراضي انهيار كامن يظهر تحت التزامن ولا يظهر في اختبار أحادي الخيط أبدًا. وضبط العلم يعطل المسار القائم على الإشارة ولا يكلف إلا دقة مهلة المحلل

وكلا العيبين يتشارك ملفًا يجعل اكتشافهما لاحقًا مكلفًا. لا واحد منهما يظهر في اختبار وظيفي مقابل خادم حسن التصرف على خيط واحد. وكلاهما يظهر في الإنتاج، مقابل TSA بعينها، تحت الحمل. وحين تربط مكتبة شبكات، اقرأ ما تفترضه افتراضاتها عن عمليتك قبل أن تفترض أنها تطابقه

مخطط نقل الطوابع الزمنية عبر libcurl في PDFium VCL يعرض curl_easy_setopt معلنة نماذجَ Pascal ثابتة آخذة بـ long وبالمؤشر تمرر الوسائط في سجلات أعداد صحيحة، وترويسة Expect الفارغة التي تكبت مصافحة HTTP 100-continue، وCURLOPT_NOSIGNAL التي تزيل مسار SIGALRM على خيوط العامل، وسقف الاستجابة على مستوى النقل
إعددان يقرران هل يكتمل الطلب: ترويسة Expect فارغة تتجنب الخوادم التي لا تجيب عن المتابعة أبدًا، وNOSIGNAL تبقي مهل حل الأسماء خارج مسار الإشارة بينما يجري التوقيع على خيط عامل

كيف تتحقق من كود لن يراه مترجمك أبدًا

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

والتقنية ميكانيكية. انسخ الوحدة إلى مجلد مؤقت، وأعد تسميتها، واستبدل كل شرط Windows، صورة {$IFDEF MSWINDOWS} وصورة {$IF DEFINED(MSWINDOWS)، برمز لا يُعرَّف قط. ثم صرّف النسخة. حين تُصرَّف الأسطر 3,828 كلها، تكون قد أثبتت أن المسار غير الوندوزي يستخدم وحدات موجودة، ويستدعي دوال الخلفية بتوقيعات مطابقة، ويشير إلى أنواع في النطاق. وليس ذلك إثباتًا أن النقل يعمل، ولن يعطيك ذلك شيء دون المنصة الهدف. إنه إثبات أن الفرع ليس معطوبًا أصلًا، وهو نمط الفشل الذي يتراكم فعلًا

والعادة الرفيقة أن تترك وحدة libcurl نفسها خالية من حوارس المنصات، فتشارك في بناء Windows العادي رغم أن لا شيء هناك يشير إليها. فيواصل البناء اليومي حراسة تركيبها وأنواعها مجانًا. فالوحدة التي لا تُصرَّف إلا على منصة لا تملكها وحدة بلا مترجم يفحصها إطلاقًا، وينطبق الاستدلال نفسه على عمل المترجمات التقاطعية كله الموصوف في مصائد المترجمات التقاطعية في Delphi وFPC

تحديد سقف ما يعود

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

وينطبق الاستدلال نفسه على الـ URL. فالخلفية تقبل المخططات التي تستطيع أن تخاطبها بمعنى فحسب، فيخفق خطأ تكوين فورًا برسالة واضحة بدل أن يُسلَّم إلى libcurl لتأويله بأي طريقة يسمح بها دعم بروتوكولاتها

موضع النقل في قصة التوقيع

الطوابع الزمنية هي الخطوة الأولى من قصة التحقق طويل الأمد لا كلها. فعلى الرمز أن يُرفق بالتوقيع، وعلى مادة التحقق أن تُسجل في مخزن أمن الوثيقة، وعلى طوابع الأرشفة أن تُجدد قبل أن يضعف الطابع الحالي. وتغطى ذلك القوس كله في التوقيعات طويلة الأمد لـ PDF بطوابع RFC 3161 وDSS

مخطط PDFium VCL لطلب طابع زمني RFC 3161 يجري من DocumentDigest عبر BuildTimeStampQuery وPostTimeStampQuery عبر libcurl إلى خادم TSA، وتستجابة DER يسقّفها النقل، ثم AttachTimeStampToken تغذي DSS وتجديد طوابع الأرشفة في التحقق طويل الأمد
الطوابع الزمنية أول خطوة في قصة التحقق طويل الأمد: يجب إرفاق الرمز، وتسجيل مادة التحقق في مخزن أمن الوثيقة، وتجديد طوابع الأرشفة قبل أن يضعف الطابع الحالي

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

وخلفيتا الطوابع الزمنية لـ Windows وغير الوندوز كلتاهما تردان مع مكوّن PDFium Delphi، تُختاران بالهدف لا بالتكوين، فينتج تطبيق Lazarus على Linux وتطبيق Delphi على Windows التوقيعَ المختوم زمنيًا نفسه عبر تمديدات مختلفة