مقالة تقنية

لماذا ترفض أدوات التحقق تواقيع PAdES: PDFium في دلفي

ترفض أداة التحقق توقيع PAdES لواحد من ثلاثة أسباب في كل حالة قمنا بتصحيحها تقريباً باستخدام مكون PDFium في دلفي: مصفوفة /ByteRange لا تزال تحتوي على حراسها الصفرية، أو أن وقت التوقيع /M ليس سلسلة تاريخ PDF جيدة التنسيق، أو أن التحديث التدريجي أسقط إدخال /Encrypt من مستند مشفر. وتنتج الثلاثة ملفات تفتح بشكل جيد، وتعرض بشكل جيد، وتفشل في اللحظة التي تقرأ فيها أداة تحقق متوافقة قاموس التوقيع

السيناريو الذي يدفع لكتابة هذه المقالة محدد للغاية. تقوم بتوقيع عقد مع تدفق العمل من مقالة توقيع PAdES B-B، ويبلغ كود الفحص الخاص بك أن التوقيع موجود وسليم هيكلياً، وكل اختبار محلي أخضر — ثم يقوم الطرف الآخر بتحميل الملف إلى منصة التحقق الخاصة به ويحصل على صليب أحمر. لا شيء في الفشل مرئي في العارض، لأن أياً من هذه العيوب الثلاثة لا يلمس محتوى الصفحة. إنها تعيش بالكامل في قاموس التوقيع ومقطورة الملف، وهو بالضبط المكان الذي تبحث فيه أدوات التحقق ولا ينظر إليه العارضون في الغالب

لماذا تعتبر ByteRange لتوقيع PDF الخاص بي غير صالحة؟

يعني رفض ByteRange دائماً تقريباً أن الحقول الأربعة لم يتم ملؤها أبداً، وليس أن النطاقات خاطئة بشكل طفيف. تعلن مصفوفة /ByteRange [A B C D] عن فترتين، من بايت A إلى A+B ومن بايت C إلى C+D، ويتطلب المعيار EN 319 142-1 الفقرة 6.3 (المتطلب k) أن يغطيا معاً الملف بأكمله باستثناء سلسلة /Contents المشفرة بالنظام الست عشري. بالأرقام: A هو 0، وC >= A+B، وC+D يساوي طول الملف، والفجوة بين B وC تحمل بالضبط سلسلة الست عشري <...> — بايتا قوسين زائد حرفين ست عشريين لكل بايت من بيانات CMS. وتستنتج أداة التحقق التي تقرأ [0 0 0 0] أن التوقيع لا يغطي شيئاً وترفضه، بغض النظر عن مدى صحة هيكل CMS داخل /Contents

يجدر فهم آليات كيفية حدوث ذلك لأن النمط نفسه موجود في كل كدسة توقيع. لا يمكن للموقع معرفة الإزاحات النهائية حتى يتم تخطيط الملف، لذا يصدر الكاتب المصفوفة مع حراس صفرية ذات عرض ثابت ويملؤها رجوعاً بعد التخطيط. في إصدارات مكون PDFium قبل 2.14.1, كان ذلك الملء الرجوعي يبحث عن نمط الحارس بدءاً من موضع /Contents — ولكن /ByteRange تقع قبل /Contents في القاموس، لذا لم يجد البحث شيئاً وفشلت عمليات الاستبدال الثلاث بصمت. تم حساب ملخص CMS على النطاقات الصحيحة، لذا كانت التشفيرات سليمة؛ وظل الإعلان عن تلك النطاقات عند الصفر، لذا رفضت كل أداة تحقق متوافقة الملف. وفشلت طوابع وقت مستند PAdES B-LTA، التي تستخدم تخطيط القاموس نفسه، بالطريقة نفسها — وهو أمر ذو صلة إذا كنت تبني على تواقيع B-LT وB-LTA طويلة المدى. يقوم الإصدار 2.14.1 بالملء من بداية كائن التوقيع، ويتم تغطية الإصلاح باختبار تراجع يحلل الملف المنتج ويؤكد الحسابات أدناه

function SignedByteRangeCoversFile(const FileName: string): Boolean;
var
  Raw: TBytes;
  Text: AnsiString;
  P, N: Integer;
  F: array[0..3] of Int64;
begin
  Raw := TFile.ReadAllBytes(FileName);
  SetString(Text, PAnsiChar(@Raw[0]), Length(Raw));
  P := Pos('/ByteRange', Text);          // first signature only
  Result := P > 0;
  if not Result then Exit;
  Inc(P, Length('/ByteRange'));
  for N := 0 to 3 do
  begin
    while (P <= Length(Text)) and not (Text[P] in ['0'..'9']) do Inc(P);
    F[N] := 0;
    while (P <= Length(Text)) and (Text[P] in ['0'..'9']) do
    begin
      F[N] := F[N] * 10 + Ord(Text[P]) - Ord('0');
      Inc(P);
    end;
  end;
  // EN 319 142-1 §6.3 req k: spans cover everything except /Contents
  Result := (F[0] = 0) and (F[2] >= F[0] + F[1]) and
            (F[2] + F[3] = Int64(Length(Raw)));
end;

عشرون سطراً من كود RTL البسيط، دون استدعاءات مكتبية، وهي تلتقط فئة الفشل بأكملها عند نقطة الإنتاج. إذا كنت ستحتفظ بتأكيد (assertion) واحد من هذه المقالة، فاحتفظ بهذا: حلل مخرجاتك الموقعة وافحص المعادلات الثلاث قبل أن يغادر الملف عمليتك

لماذا تعلم أدوات التحقق وقت التوقيع /M كمشوه؟

ترفض أدوات التحقق الصارمة وقت التوقيع الذي لا يمثل سلسلة تاريخ PDF كاملة، والجزآن اللذان يغيبان غالباً هما بادئة D: وعلامة إزاحة UTC. ويحدد معيار ISO 32000-1 الفقرة 7.9.4 تنسيق التاريخ كـ D:YYYYMMDDHHmmSS متبوعاً بإزاحة — Z لـ UTC، أو علاقة +HH'mm' موقعة به. ويحلل الرقم المجرد 20260709143000 كتاريخ للقارئ المتساهل، ولكن أداة التحقق التي تطبق القواعد النحوية حرفياً ترى سلسلة مشوهة في حقل إلزامي التنسيق وتعلم التوقيع. كتبت إصدارات مكون PDFium قبل 2.14.4 إدخال /M لـ TPdf.SignPades وSignPadesBytes في ذلك الشكل المجرد بالضبط؛ ومنذ 2.14.4 يحمل الإدخال بادئة D: والعلامة Z، لذا يقرأ وقت التوقيع المعلن D:20260709143000Z

تلتصق ملاحظتان عمليتان بهذا الحقل. أولاً، اكتب UTC وقل ذلك: طابع الوقت بدون علامة إزاحة يجبر أداة التحقق على تخمين علاقة المنطقة الزمنية، وتعامل الفقرة 7.9.4 الإزاحة كجزء من التنسيق وليس كشيء اختياري. ثانياً، تذكر ما هو /M — الوقت المعلن من قبل الموقع، وهو ادعاء وليس إثباتاً. تتحقق أداة التحقق من تنسيقه هنا، وليس صدقه؛ ويأتي الوقت القابل للإثبات من طابع وقت RFC 3161 timestamp عند PAdES B-T وما فوق. تنسيق الحقل بشكل صحيح والوثوق به هما قراران منفصلان، وتفرض أدوات التحقق الأول فقط

لماذا ترفع SignPades استثناء EPadesCrypto على ملف PDF مشفر؟

يرفض مكون PDFium توقيع مستند مشفر لأن البديل هو إنتاج ملف سيدمره القراء المتوافقون عند الفتح. يتم إلحاق توقيع PAdES كتحديث تدريجي، ويتطلب معيار ISO 32000-1 الفقرة 7.5.6 أن تحمل المقطورة الجديدة لقسم التحديث كل إدخال للمقطورة السابقة باستثناء /Prev — وفي مستند مشفر يتضمن ذلك /Encrypt. وإذا أسقطته، تعلن المقطورة الأخيرة أن الملف غير مشفر، لذا يحلل القارئ المتوافق الجسم المشفر كنص عادي ويحصل على هراء. والأسوأ من ذلك، أن التدفقات والسلاسل التي يلحقها الموقع يجب أن تكون مشفرة بمفتاح المستند لكي تكون قانونية، وهو ما لا يستطيع حاقن التوقيع ذو النص العادي القيام به. لا توجد طريقة لإلحاق توقيع نص عادي صالح بملف مشفر، لذا منذ الإصدار 2.14.2 ترفع TPdf.SignPades وSignPadesBytes وInjectPadesDssMarkers الاستثناء EPadesCrypto بدلاً من إصدار نتيجة تالفة أو غير قابلة للتحقق

try
  if not Pdf.SignPades('contract-signed.pdf', AThumbprint) then
    Writeln('Signing reported failure');
except
  on E: EPadesCrypto do
  begin
    // e.g. 'SignPadesBytes: the source document is encrypted;
    //       remove encryption before signing'
    Writeln('Cannot sign: ', E.Message);
  end;
end;

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

كيف يمكن لخطأين أن يصدقا على بعضهما البعض

اختبأ عيب ByteRange طالما اختبأ لأن أداة التحقق الخاصة بنا كانت خاطئة بطريقة مكملة، وهذا هو الدرس الأكثر أهمية في هذه المقالة. تطلب فحص التغطية in ValidatePadesCompliance required the second span to start exactly at A+B — a zero gap — which misclassifies the standard layout where the gap holds the /Contents hex string. So the in-house validator rejected precisely the conforming layout and the in-house generator never produced one, and end-to-end sign-then-validate pipelines happened to stay green. Each bug denied the test suite the counterexample that would have exposed the other. Version 2.14.1 fixed both sides in the same release: the generator back-fills all four fields, and the validator accepts C >= A+B with C+D equal to the file length

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

var
  R: TPadesValidationResult;
begin
  Pdf.FileName := 'contract-signed.pdf';
  Pdf.Active := True;
  R := Pdf.ValidatePades;
  if ppeiByteRangeNotCoveringFile in R.Issues then
    Writeln('ByteRange does not cover the file');
  if not R.IsCompliant then
    Writeln('Structural issues present: do not ship this file');
end;

قائمة فحص الرفض للمخرجات الموقعة

عندما ترفض منصة ما توقيع PAdES الخاص بك، تحقق من الأسباب الهيكلية الرخيصة قبل الاشتباه في الشهادات أو سلاسل الثقة. اقرأ مصفوفة /ByteRange وتحقق من المعادلات الثلاث: الحقل الأول صفر، وتبدأ الفترة الثانية عند أو بعد نهاية الأولى، وتنتهي الفترة الثانية عند نهاية الملف تماماً. واقرأ إدخال /M وتحقق من أنه سلسلة تاريخ كاملة وفقاً للفقرة 7.9.4 مع بادئة D: وعلامة إزاحة. وتأكد من أن مستند المصدر لم يكن مشفراً عند إلحاق التوقيع — وإذا قامت كدستك بتوقيعه على أي حال دون شكوى، فعامل ذلك الصمت كخطأ في الكدستك. تعمل الفحوصات الثلاثة على البايتات الخام في أجزاء من الثانية، وفي خبرتنا هي تفسر الرفض أكثر بكثير من أي سبب تشفيري

تُشحن استدعاءات التوقيع والفحص والتحقق المستخدمة هنا — SignPades، وValidatePades، وفحوصات امتثال PAdES بحسابات ByteRange المصححة وتنسيق التاريخ وبوابة التشفير — مع مكون PDFium لدلفي وC++Builder ولازاروس