مقال تقني

قوائم الثقة في eIDAS وتواقيع PDF المؤهلة

الحكم بأن توقيع PDF مؤهل بموجب eIDAS يعني الإجابة على سؤال لا علاقة له بالتشفير: هل صدرت الشهادة من خدمة ثقة أدرجتها دولة عضو كمؤهلة، في اللحظة التي صُنع فيها التوقيع. والجواب يعيش في قائمة ثقة، وثيقة XML تنشر لكل إقليم، وكل قيمة تلك الوثيقة معلقة على أصالتها. لذا يرفض مكون PDFium النظر داخل واحدة حتى يشهد عليها أحدهم. يسلم TPdfEuropeanTrustedList.ParseAuthenticated البايتات الخام كاملة إلى IPdfTrustedListAuthenticator مزود من المستدع قبل أن يحلل خدمة واحدة، ولا ينشئ لقطة إلا إذا اجتاز ذلك المصادق صراحة

مسار المصادقة قبل التحليل لقائمة ثقة أوروبية في Delphi: XML الخام من جلب جديد أو لقطة مخزنة يمر عبر IPdfTrustedListAuthenticator قبل أن يحلل TPdfEuropeanTrustedList أي شيء
القوائم الجديدة والمخزنة تقابل المصادق نفسه، ولا توجد لقطة إلا بعد اجتيازه

ذلك الترتيب هو التصميم. وكل شيء آخر في هذه الميزة يترتب عليه، بما في ذلك الأجزاء التي تبدو غير مريحة

المحلل ليس موثوقا

قائمة ثقة تحلل بنظافة تخبرك أن XML مكون جيدا. ولا تخبرك شيئا عن من كتبها. وبما أن القائمة هي ما يستند إليه قرار حالة التأهيل كله، فإن قبول واحدة لأنها حلت سيجعل القرار بلا معنى: مهاجم يستطيع استبدال القائمة يستطيع إعلان جهة إصدار شهاداته هو مؤهلة

وينطبق الاستدلال نفسه على التخزين المؤقت، وهذا هو الفخ الجدير بالتسمية. يخزن مخزن اللقطات XML الأصلي مع ملخص SHA-256، ومن السهل التعامل مع ملخص مطابق عند التحميل كدليل على أن القائمة حقيقية. وهو ليس كذلك. فالملخص المحسوب من العملية نفسها التي خزنت الملف، بلا أي مفتاح، يتحقق فقط من أن البايتات لم تتغير منذ كتبتها؛ وإن كانت القائمة محتالة وقت تخزينها فالملخص يؤكد أنها القائمة المحتالة نفسها. لذا يمر تحميل لقطة مخزنة عبر المصادق نفسه الذي يمر به تحليل قائمة جديدة. والسلامة والأصالة خاصيتان مختلفتان وواحدة منهما فقط تحتاج مفتاحا

uses
  FPdfTrustedList;

type
  TListAuthenticator = class(TInterfacedObject, IPdfTrustedListAuthenticator)
  public
    function Authenticate(const XmlData: TBytes;
      out AuthenticationDetails: string): Boolean;
  end;

function TListAuthenticator.Authenticate(const XmlData: TBytes;
  out AuthenticationDetails: string): Boolean;
begin
  // سياستك تعيش هنا: تحقق من توقيع XMLDSIG المغلف مقابل شهادة
  // توقيع القائمة التي ثبتتها خارج النطاق،
  // وصِف ما فحصته لأثر التدقيق
  Result := VerifyEnvelopedXmlSignature(XmlData, FPinnedListSigner);
  if Result then
    AuthenticationDetails := 'XMLDSIG verified against pinned LOTL signer';
end;

var
  List: TPdfEuropeanTrustedList;
  Cache: TFileStream;
begin
  List := TPdfEuropeanTrustedList.ParseAuthenticated(RawXml,
    TListAuthenticator.Create, TPdfTrustedListOptions.Default);
  // اللقطة توجد فقط لأن المصادق قال نعم
  Cache := TFileStream.Create('tl-de.snapshot', fmCreate);
  try
    List.SaveCache(Cache);
  finally
    Cache.Free;
  end;
end;

المتحقق لا يملك سياسة الشبكة

ليس من شأن متحقق PAdES أن يقرر كيف يصل إلى قائمة قوائم الثقة، وهل يمر عبر وسيط، وكم مرة يعيد المحاولة، وماذا يفعل عندما يكون إقليم غير قابل للوصول. تلك قرارات تطبيق ونشر، وفي البيئات المنظمة تخضع للتدقيق كثيرا. لذا تصل التحديثات عبر IPdfTrustedListSource، الذي يسلم عنوان URI وسقف بايتات ويعيد بايتات

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

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

حدود المحلل، ولا DTD إطلاقا

يسقف TPdfTrustedListOptions حجم XML وعدد الرموز وعمق التعشيش وعدد الخدمات وعدد الشهادات وحجم شهادة مفردة، بتوابع من الشكل Default توفر قيمًا قابلة للاستخدام. قوائم الثقة وثائق منشورة ذات حجم متوقع، فالحدود رخيصة الضبط ولا توجد قائمة مشروعة تحتاج تجاوزها

وعلى حدة وبلا شرط، يرفض المحلل تعريفات DTD والكيانات. ذلك يغلق كلا من إنكار الخدمة بتوسيع الكيانات وطريق كشف الكيانات الخارجية في رفض واحد، ولا يكلف شيئا لأن قوائم الثقة لا تستخدم كيانات. وينبغي ضبط أي محلل XML قابل للوصول من مدخلات غير موثوقة على هذا النحو؛ والفرق هنا أن الرفض غير قابل للضبط، فلا يمكن إطفاؤه بتغيير خيار حسن النية

حالة التأهيل تسجل بجوار ثقة السلسلة لا تدمج فيها

وجهة التقويم منفصلة عمدا. يأخذ TPadesTrustValidationOptions.QualifiedTrustEvaluator واجهة IPdfQualifiedTrustEvaluator، التي تنفذها لقطة قائمة الثقة. وأثناء التحقق يستلم المقوِّم شهادة الورقة والسلسلة ووقت تحقق، ويطابق شهادات الخدمات بمقارنة DER تامة ضد الموقّع والسلسلة، ويجمع حالة الخدمة ومعرف نوع الخدمة وعناوين URI المؤهلة عند تلك اللحظة، ويعيد سجل تقويم

تستقر النتيجة في موضعين على كل توقيع: QualifiedTrustStatus كحالة إجمالية، وQualifiedTrust كتقويم كامل بالإقليم واسم المزود واسم الخدمة ومعرف النوع والحالة ووقت بدء الحالة. وما لا فعله هو تغيير CertificateTrustStatus. ثقة سلسلة النظام وحالة التأهيل تجيبان عن سؤالين مختلفين، وتقرير يدمجهما لا يستطيع التمييز بين «موثوق وغير مؤهل» و«مؤهل والسلسلة لا تتحقق»، وكلاهما حقيقي وكلاهما يحتاج معالجة مختلفة

تقويم الثقة المؤهلة في PAdES في Delphi: يطابق IPdfQualifiedTrustEvaluator شهادات الخدمات بمقارنة DER تامة ويملأ QualifiedTrustStatus وQualifiedTrust بينما يبقى CertificateTrustStatus دون مساس
ثقة السلسلة وحالة التأهيل في eIDAS تسجلان جنبا إلى جنب كي تبقى النتيجتان مرئيتين
var
  Options: TPadesTrustValidationOptions;
  Report: TPadesValidationResult;
  I: Integer;
begin
  Options := TPadesTrustValidationOptions.Default;
  Options.CheckRevocation := True;
  Options.QualifiedTrustEvaluator := List;      // اللقطة المصادَق عليها
  Options.QualifiedValidationTime := SigningTime; // ليس Now
  Report := Pdf.ValidatePadesTrust(Options);

  for I := 0 to High(Report.Signatures) do
    if Report.Signatures[I].QualifiedTrustStatus = pcsValid then
      Writeln(Format('signature %d qualified by %s / %s (%s)',
        [I, Report.Signatures[I].QualifiedTrust.Territory,
         Report.Signatures[I].QualifiedTrust.ProviderName,
         Report.Signatures[I].QualifiedTrust.ServiceName]))
    else if Report.Signatures[I].QualifiedTrustStatus = pcsIndeterminate then
      // لا خدمة مطابقة، أو اللقطة لا تستطيع الإجابة عن هذا الوقت
      Writeln(Format('signature %d: qualified status undetermined', [I]));
end;

لماذا وقت التحقق ليس الآن

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

تحمل القائمة ما يلزم لذلك: كل سجل خدمة لديه وقت بدء حالة وعلم يميز المدخلات التاريخية عن الحالية، ويجمعها المقوِّم مقابل الوقت الذي تزوده. وعمليا يأتي ذلك الوقت من ختم زمني موثوق على التوقيع لا من وقت التوقيع المزعوم في CMS، ولهذا تهم مواد التحقق طويلة الأمد حتى لسؤال يبدو بحث سياسة؛ ووجهة الختم الزمني وDSS مشمولة في مقال التواقيع طويلة الأمد

ما ما يزال عليك بناؤه

ثلاثة أشياء، ولا شيء منها ينتمي إلى مكتبة PDF. المصادق، أي التحقق الفعلي بـ XMLDSIG مقابل شهادة توقيع قائمة حصلت عليها عبر قناة تثق بها. وسياسة الجلب، أي كيف وكم مرة تجدد، وماذا يفعل تطبيقك عندما يفشل التجديد. والنطاق الإقليمي، أي أي قوائم تحمل أصلا، وهو قرار عمل حول أي الدول الأعضاء يوقّع نظراؤك فيها

ما تحصل عليه من المكون هو الجزء الذي يسهل الخطأ فيه بخفة: ترتيب المصادقة قبل التحليل، وتحليل XML محدود وخال من الكيانات، وثوابت تحديث رتيبة، ومطابقة خدمات بـ DER تامة، وتقويم الحالة التاريخية، ونتيجة تبقى منفصلة عن ثقة السلسلة العادية. وإن كانت مشكلتك المباشرة أكثر بدائية، أن متحققا يرفض توقيعا تظنه سليما، فالأسباب الاعتيادية مصنفة في لماذا يرفض المتحققون تواقيع PAdES، وسطح فحص التواقيع موصوف في فحص التواقيع ومستويات PAdES. وقدرات المكون مدرجة في صفحة منتج PDFium Delphi component