لقد تلقيت ملف PDF موقعاً وبحاجة إلى إظهار، في عارضك الخاص، من قام بتوقيعه، ومتى تم توقيعه، وهل يغطي التوقيع الملف بأكمله، وإلى أي مدى يذهب نحو الامتثال طويل الأجل. يجيب مكون PDFium لـ Delphi و Lazarus عن الأسئلة الأربعة جميعاً من خلال استدعاءات القراءة فقط: حيث تعرض FPDF_GetSignatureCount وعائلة FPDFSignatureObj_* قاموس التوقيع، وتقوم دالة TPdf.ValidatePades بتصنيف مستوى PAdES الأساسي. هذا هو المقال الأول من ثلاثة مقالات حول تواقيع PDF باستخدام PDFium؛ ويغطي المقالان التاليان إنشاء توقيع B-B وإضافة طوابع زمنية طويلة الأجل. ومع ذلك، هناك حد واحد ينتمي إلى المقدمة: كل شيء هنا هو مجرد فحص، وقراءة ما يدعيه التوقيع هي مهمة مختلفة عن التحقق من تشفيره أو تحديد ما إذا كنت تثق بالموقع
لماذا لا يعد قاموس توقيع PDF مجرد كتلة من البايتات
توقيع PDF هو قاموس، وليس مرفقاً غامضاً، ويخبرك أهم إدخالين فيه بمدى الحماية الفعلية للملف. يحدد معيار ISO 32000-1 الفقرة 12.8 قاموس التوقيع بإدخال /ByteRange وإدخال /Contents. يحمل /Contents بنية CMS SignedData مشفرة بنظام سداسي عشري (RFC 5652)، وهو الغلاف المشفر الذي يحمل شهادة الموقع، والسمات الموقعة، وقيمة التوقيع نفسها. بينما /ByteRange هو الجزء الذي يقلل المطورون من تقديره: فهو عبارة عن مصفوفة من فترتي إزاحة وطول تغطيان معاً الملف بأكمله باستثناء سلسلة /Contents السداسية العشرية. تلك الفجوة هي بالضبط المكان الذي تجلس فيه بايتات التوقيع، والفترتان على كلا الجانبين هما بالضبط ما يلتزم به التوقيع
تصميم ByteRange هو ما يجعل الحفظ المتزايد قابلاً للتدقيق. ولأنه لا يمكن للموقع تجزئة (hash) بايتات التوقيع التي لا توجد بعد، يتم تقسيم الملف حول العنصر النائب /Contents ويتم تجزئة كل شيء آخر في التوقيع. ويعد التوقيع الذي لا يصل ByteRange الخاص به إلى نهاية الملف علامة تحذير: فالمحتوى الملحق بعد النطاق المغطى، من خلال تحديث متزايد لاحق، لن يكسر التوقيع على الرغم من أنه يغير ما يراه القارئ. لذا فإن أول شيء يفحصه المفتش الجاد ليس من وقع، بل ما إذا كان التوقيع يغطي البايتات التي يبدو أنه يقرها
قراءة قاموس التوقيع باستخدام واجهة برمجة تطبيقات القراءة فقط لـ PDFium
يظهر مكون PDFium قاموس التوقيع من خلال عضوين للقراءة فقط: SignatureCount وسجل Signature[Index]. خلف الكواليس، يستدعون FPDF_GetSignatureCount و FPDF_GetSignatureObject وموصلات FPDFSignatureObj_* لـ /SubFilter و /ByteRange و /Contents و /Reason ووقت التوقيع. "القراءة فقط" هي العبارة الحيوية هنا: يمكن لـ PDFium سرد وقراءة التواقيع ولكنه لا يملك واجهة برمجة تطبيقات لإنشاء أو كتابة أحدها، ولهذا السبب يتم تنفيذ جانب التوقيع في هذه السلسلة بواسطة المكتبة نفسها وليس بواسطة PDFium
var
Pdf: TPdf;
i: Integer;
Sig: TPdfSignature;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := 'contract-signed.pdf';
Pdf.Active := True;
for i := 0 to Pdf.SignatureCount - 1 do
begin
Sig := Pdf.Signature[i];
Writeln('SubFilter : ', Sig.Encoding); // ETSI.CAdES.detached, adbe.pkcs7.detached, ...
Writeln('Signed at : ', Sig.Time); // signer date string, e.g. D:20260708120000+02'00'
Writeln('Reason : ', Sig.Reason);
Writeln('CMS length: ', Length(Sig.Content)); // raw DER SignedData taken from /Contents
Writeln('DocMDP : ', Sig.Permission); // 0 = no certification, 1..3 = MDP level
end;
finally
Pdf.Free;
end;
end;
يطابق كل سجل TPdfSignature القاموس مباشرة. Encoding هي /SubFilter، وهو الحقل الأكثر تشخيصاً، لأنه يسمي معالج التوقيع ويفصل على الفور توقيع ETSI.CAdES.detached الحديث عن التوقيع القديم أو المحظور. Time هو وقت التوقيع المعلن من قبل المؤلف كسلسلة تاريخ PDF، وهو ادعاء من قبل الموقع وليس وقتاً موثوقاً به. Content هو CMS SignedData الخام، ويعرض Permission مستوى شهادة DocMDP (0 للتوقيع العادي، ومن 1 إلى 3 لتوقيع الشهادة الذي يقفل التغييرات اللاحقة). الحقل الوحيد الذي لا يظهره السجل هو ByteRange الذي تم تحليله، هذا الحذف متعمد، لأن ValidatePades يقوم بحساب تغطية ByteRange نيابة عنك بدلاً من إجبارك على إعادته يدوياً
ما هو الفرق بين PAdES B-B و B-T و B-LT و B-LTA؟
تشكل مستويات PAdES الأساسية الأربعة سلماً من توقيع صالح بالحد الأدنى إلى توقيع مبني للبقاء لعقود من الأرشفة، ويحتوي كل مستوى بدقة على المستوى الذي يليه. يحددها معيار ETSI EN 319 142-1 كـ B-B و B-T و B-LT و B-LTA. التوقيع B-B (الأساسي) هو التوقيع بالإضافة إلى السمات الموقعة الإلزامية ولا شيء غير ذلك. ويضيف B-T (طابع زمني) طابعاً زمنياً موثوقاً RFC 3161 فوق التوقيع، بحيث يتم إثبات لحظة التوقيع من قبل سلطة الطابع الزمني بدلاً من ادعائها بواسطة ساعة الموقع. ويقوم B-LT (طويل الأجل) بتضمين مواد التحقق — سلسلة الشهادات، واختيارياً استجابات OCSP أو CRL — داخل الملف، بحيث يمكن الاستمرار في التحقق من التوقيع بعد سنوات عندما تزول البنية التحتية المصدرة. ويغلف B-LTA (طويل الأجل مع طابع زمني للأرشيف) طابعاً زمنياً للمستند حول تلك المواد، مما يحمي البيانات طويلة الأجل نفسها ويمنحك نقطة لإعادة الطابع الزمني قبل أن تتقادم التشفيرات الأساسية
القراءة العملية تتعلق بالآفق الزمني. يجيب توقيع B-B على "هل قام شخص ما بتوقيع هذا". ويجيب B-T على "ومتى، بشكل مثبت". ويجيب B-LT على "وهل لا يزال بإمكاني التحقق منه بعد انتهاء صلاحية الشهادات". ويجيب B-LTA على "وهل سيظل هذا التحقق صالحاً بعد عشرين عاماً". تختار الملفات التنظيمية درجة: تتطلب العديد من سياقات الفواتير الإلكترونية و eIDAS الحصول على B-T على الأقل، وتصل متطلبات الأرشفة إلى B-LT أو B-LTA. إن معرفة الدرجة التي يصل إليها المستند بالفعل، قبل قبوله أو رفضه، هو الهدف الكامل من خطوة الفحص
الكشف عن المستوى الأساسي باستخدام TPdf.ValidatePades
يقلل مكون PDFium مسألة المستوى بأكملها إلى استدعاء واحد. تعيد TPdf.ValidatePades سجل TPadesValidationResult الذي يكون حقل Level فيه عبارة عن TPadesLevel — وهي plNone أو plUnknown أو plB_B أو plB_T أو plB_LT أو plB_LTA — جنباً إلى جنب مع مجموعة من المشكلات وعدد التواقيع وعدد الطوابع الزمنية للمستند. يتم استنتاج المستوى بشكل مطرد: يحدد المدقق B-B أولاً، ثم يرقيه إلى B-T إذا كان طابع توقيع أو طابع مستند موجوداً، وإلى B-LT إذا كان الكتالوج يحمل /DSS مع الشهادات وعلامة /Extensions /ESIC من المستوى 1، وإلى B-LTA إذا كان طابع مستند وعلامة ESIC من المستوى 2 موجودين معاً. يجعل مساعدان النتيجة قابلة للتنفيذ: IsCompliant تكون True فقط عندما يصل المستوى إلى B-B على الأقل وتكون مجموعة المشكلات فارغة، وتتيح لك IsCompliantAt تأكيد حد سياسة أدنى مثل plB_T
var
Pdf: TPdf;
R: TPadesValidationResult;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := 'contract-signed.pdf';
Pdf.Active := True;
R := Pdf.ValidatePades;
case R.Level of
plNone: Writeln('No PAdES signature present');
plUnknown: Writeln('Signature present but level undeterminable');
plB_B: Writeln('PAdES B-B (basic)');
plB_T: Writeln('PAdES B-T (trusted timestamp)');
plB_LT: Writeln('PAdES B-LT (long-term material embedded)');
plB_LTA: Writeln('PAdES B-LTA (archive timestamp)');
end;
Writeln('Signatures : ', R.SignatureCount);
Writeln('DocTimeStamps: ', R.DocTimeStampCount);
if R.IsCompliantAt(plB_T) then
Writeln('Meets the B-T policy floor')
else
Writeln('Below the required B-T level');
finally
Pdf.Free;
end;
end;
لماذا يعد adbe.pkcs7.sha1 خيار SubFilter محظوراً؟
لأن خوارزمية SHA-1 مكسورة ومعالج adbe.pkcs7.sha1 يخبزها فيه. هذا الـ SubFilter يجزئ المستند مسبقاً بـ SHA-1 قبل تغليفه في PKCS#7، وظلت SHA-1 عرضة للتصادمات لسنوات، لذا فإن البند 6.3 من معيار EN 319 142-1 يحظرها تماماً للتوقيع الأساسي. تثير ValidatePades خطأ ppeiForbiddenSubFilter عندما ترى adbe.pkcs7.sha1 أو adbe.x509.rsa_sha1,وتثير ppeiBadDigestAlgorithm عندما تستخدم CMS نفسها MD5 أو SHA-1 كخلاصة رسالة (البند 6.2.1). هذان فحصان متميزان يلتقطان نفس الفئة من الضعف في طبقتين مختلفتين
تحتوي مجموعة المشكلات على 26 عضواً في المجمل، وتلك التي ستواجهها غالباً تتركز حول البنية والتغطية. ppeiByteRangeNotCoveringFile هو فحص التغطية الموصوف سابقاً. وينطلق ppeiForbiddenCertKey عندما يحمل قاموس التوقيع إدخال /Cert، وهو ما يحظره PAdES لأن السلسلة يجب أن تعيش داخل CMS SignedData.certificates بدلاً من ذلك. وتحدد ppeiMissingSigningCertificate و ppeiMissingContentType و ppeiMissingMessageDigest السمات الموقعة الإلزامية الغائبة، ويلتقط ppeiDetachedContentViolation التوقيع الذي يضمن المحتوى الموقع بشكل خاطئ بدلاً من فصله. إن سرد المجموعة يحول الرفض المجرد إلى تشخيص يمكنك تسجيله
var
R: TPadesValidationResult;
Issue: TPadesValidationIssue;
begin
R := Pdf.ValidatePades;
if R.Issues <> [] then
for Issue := Low(TPadesValidationIssue) to High(TPadesValidationIssue) do
if Issue in R.Issues then
Writeln('Issue: ',
GetEnumName(TypeInfo(TPadesValidationIssue), Ord(Issue)));
end;
ما لا يفحصه ValidatePades
تقوم ValidatePades بمدى صحة البنية، وليس الثقة، والخلط بين الاثنين هو الخطأ الخطير. نتيجة plB_LTA تعني أن المستند يحتوي على توقيع B-LTA مصاغ بشكل جيد مع جميع السمات والمواد والطوابع الزمنية المفروضة في أماكنها الصحيحة — ولا يعني ذلك أن التوقيع صالح تشفيراً، أو أن سلسلة الشهادات تتصل بجذر تثق به، أو أنه لم يتم إلغاء أي شهادة في السلسلة. لا يقوم المدقق عمداً بأي تحقق تشفيري: فهو لا يعيد حساب التوقيع عبر ByteRange، ولا يبني أو يقيم سلسلة الثقة، ولا يفحص حالة إلغاء OCSP أو CRL. هذا الانقسام مقصود ومفيد، لأن الفحص الهيكلي سريع، ومحدد بالكامل، ولا يحتاج إلى مفاتيح أو شبكة أو تشفير نظام أساسي، لذا تعمل ValidatePades بشكل متطابق على ويندوز ولينكس وماك ككود Pascal نقي على بايتات الملف. وعلى النقيض من ذلك، فإن التحقق من سلسلة الثقة لا ينفصل عن السياسة — أي الجذور تثق بها، وكيف تجلب الإلغاء، ومدى صرامة تحملك للطوابع الزمنية — وينتمي إلى مرحلة لاحقة تعتمد على مخزن شهادات النظام الأساسي، لذا تعامل مع اجتياز ValidatePades كبوابة ضرورية بأن التوقيع مصاغ بشكل صحيح، ثم سلم التوقيع السليم هيكلياً إلى التحقق التشفيري الحقيقي قبل الاعتماد عليه
تلك التمريرة الهيكلية هي المكان المناسب للبدء، وهي تقترن بشكل طبيعي بالفحوصات الأوسع للقراءة فقط في تدقيق مخاطر أمان PDF باستخدام مكون PDFium ومع عمل مطابقة التنسيق مثل التحقق من صحة مستندات PDF/X الجاهزة للطباعة. بمجرد أن تتمكن من قراءة التوقيع وتصنيفه، فإن الخطوة التالية هي إنتاج توقيع: يغطي المقال الثاني في هذه السلسلة توقيع ملفات PDF باستخدام PAdES B-B، ويمد المقال الثالث ذلك إلى الطوابع الزمنية الموثوقة وتواقيع B-LT and B-LTA طويلة الأجل. يعد فحص التوقيع للقراءة فقط ومصنف ValidatePades المعروضين هنا جزءاً من مكون PDFium لـ Delphi و C++Builder و Lazarus