يستخرج PDF Library for Delphi (PDFlibPas) شهادات توقيع PDF بتجوال DER خالص فوق CMS SignedData المخزّن في /Contents، دون أي سند إلى CryptoAPI. ومنذ v3.539.10 تحدد كل قراءة متداخلة بحدود عنصرها الأم، ويقص الحشو الصفري الذي يلي CMS عند الطول الذي يعلنه CMS، وتنثق معرفات الكائنات OID قوسها الفرعي الأول المدمج بترميز base-128. وقاعدة الحدود وإصلاح OID كلاهما استبدل كوداً كان يجيب إجابات خاطئة دون أن يرفع خطأً، أما قاعدة الحشو فتبقي القارئ الأشد صرامة من رفض تواقيع حقيقية
جهة القراءة أهم مما تبدو. أدوات التحقق طويل الأمد لا بد أن تسحب شهادة الموقّع ومصدريه من توقيع قائم قبل أن تجلب بيانات الإبطال، وتقرير التدقيق لا بد أن يقول من وقّع، وبناء Lazarus على لينكس لا يملك دوال رسائل ويندوز التي يستند إليها. والمحلل في هذا الموقع نادراً ما ينهار أمام مدخل سيئ. صيغة الفشل المؤلمة هي عدّ شهادات يحتسب بايتات لجارٍ، أو مطابقة موقّع على حقل خاطئ، أو OID يتحول بهدوء إلى OID آخر. فخط أنابيب تواقيع مبني فوق ذلك يبلّغ هراءً بثقة كاملة
قراءة شهادات الموقّع من PDF موقّع
خمس طرق في TPDFlib تغطي جهة القراءة، وكلها تأخذ InputFile, Password, FieldName: كل استدعاء يفتح الملف للقراءة فقط، ويجيب، ثم يغلقه من جديد. GetSignatureEmbeddedCertificateCount و GetSignatureEmbeddedCertificateDER تعدّان مجموعة الشهادات بترتيب الترميز، و GetSignatureSignerCertificateDER تعيد الشهادة التي أنتجت SignerInfo معيناً، و GetSignatureCertificateChainLength / GetSignatureCertificateChainDER تجولان من ذلك الموقّع نحو أبعد مصدر تحمله التواقيع نفسها. الفهارس تبدأ من الصفر. واحفظ النتائج في AnsiString، وهذا سبب إعادتها للمكتبة على هذه الصورة: كتلة DER تمر عبر string أو TStrings تخوض تحويل مجموعة محارف وتعود تالفة
uses
SysUtils, Classes, PDFlibrary;
procedure SaveDer(const FileName: string; const Der: AnsiString);
var
Fs: TFileStream;
begin
Fs := TFileStream.Create(FileName, fmCreate);
try
if Der <> '' then
Fs.WriteBuffer(Der[1], Length(Der));
finally
Fs.Free;
end;
end;
const
Src = 'contract-signed.pdf';
Field = 'Signature1';
var
Pdf: TPDFlib;
ChainLen, I: Integer;
SignerDer, LastDer: AnsiString;
begin
Pdf := TPDFlib.Create;
try
WriteLn('Certificates in the CMS: ',
Pdf.GetSignatureEmbeddedCertificateCount(Src, '', Field));
SignerDer := Pdf.GetSignatureSignerCertificateDER(Src, '', Field, 0);
if SignerDer = '' then
raise Exception.Create('signer certificate missing or not matched');
SaveDer('signer.cer', SignerDer);
ChainLen := Pdf.GetSignatureCertificateChainLength(Src, '', Field, 0);
for I := 0 to ChainLen - 1 do
SaveDer(Format('chain-%d.cer', [I]),
Pdf.GetSignatureCertificateChainDER(Src, '', Field, 0, I));
if ChainLen > 0 then
begin
LastDer := Pdf.GetSignatureCertificateChainDER(Src, '', Field, 0, ChainLen - 1);
WriteLn('Next issuer, if any: ', Pdf.GetCertificateIssuerURLs(LastDer));
end;
finally
Pdf.Free;
end;
end;
أمران في ذلك الناتج يستحقان انتباهاً. العدد 0 ليس تشخيصاً: حقل مفقود، وكلمة سر خاطئة، وكتلة ليست DER، و SignedData يتجاهل ببساطة مجموعة الشهادات الاختيارية، كلها تعود 0 أو سلسلة فارغة، فسجّل اسم الحقل إلى جانب الرقم. وسلسلة تنتهي قبل شهادة صادرة عن ذاتها ليست خطأً هي أيضاً. بانئ السلاسل لا يستخدم إلا الشهادات المضمّنة في التواقيع، فيجب جلب المصدرين الباقيين عبر العناوين التي يبلّغ عنها GetCertificateIssuerURLs
كم من /Contents يعود فعلاً لـ CMS؟
البادئة التي يعلنها SEQUENCE الخارجي وحده هي ما يعود لـ CMS، و PLTrimCMSPadding يقص كل ما بعدها. الموقّع يحجز سلسلة /Contents السداسية قبل وجود CMS أصلاً، لأن /ByteRange الموصوفة في ISO 32000-1 §12.8.1 يجب أن تثبت أولاً، فيُقاس الحجز بسخاء ويبقى الذيل غير المستخدم أصفاراً. يقرأ PLTrimCMSPadding أول TLV، ويشترط وسم $30، ويعيد البايتات حتى نهاية ذلك العنصر؛ وما لا يبدأ بـ SEQUENCE مشكّل تشكيل سليم يعود فارغاً. ذلك المستوى الأعلى هو الموضع الوحيد الذي تكون فيه البايتات اللاحقة مشروعة، والتمييز مهم للقسم التالي: قاعدة صارمة «يجب أن يستهلك العنصر المخزن كله» لرفضت كل توقيع واقعي، بينما قاعدة متساهلة تُطبق على كل عمق تسمح للحقول المتداخلة بقراءة بايتات لا تملكها
لماذا يحتاج قارئ DER إلى إزاحة نهاية الأب؟
العنصر المتداخل صالح فقط إذا انتهى داخل أمه، والفحص ضد نهاية المخزن لا يثبت ذلك. DERReadTLV منخفضة المستوى في PDFlibASN1 تحدد كل عنصر بسلسلة كاملة، وهذا هو الفحص الصائب للكائن الأقصى والفحص الخطأ لكل ما تحته. تصوّر SignerInfo تعلن issuerAndSerialNumber طولاً 40 بايت بينما اسم المصدر issuer Name داخلها يدّعي 60. كل بايت ما زال في المخزن، فيقبل القارئ المحدد بالمخزن الاسمَ، ويقرأ الرقم التسلسلي من خوارزمية الهضم التي تليه، ثم يقارن ذلك الثنائي بالشهادات المضمّنة. وقبل v3.539.10 كان تجوال CMS يقرأ بهذه الطريقة بالضبط. الإصلاح غلاف صغير يحمل موضع نهاية الأب إلى كل قراءة
function ReadTLVWithin(const Data: AnsiString; ParentEnd: Integer;
var Offset: Integer; out Tag: Byte; out Start, Len: Integer): Boolean;
begin
Result := False;
// لم يبق شيء داخل الأب: ارفض بدء قراءة
if (Offset < 1) or (Offset >= ParentEnd) then
Exit;
if not DERReadTLV(Data, Offset, Tag, Start, Len) then
Exit;
// Offset الآن بعد العنصر بموضع واحد؛ يجب ألا يتجاوز الأب
Result := Offset <= ParentEnd;
end;
// كل مستوى يسجل نهايته الخاصة ويسلّمها إلى الأسفل:
// OuterEnd := end of ContentInfo (RFC 5652 section 3)
// ExplicitEnd := end of content [0] EXPLICIT
// ContentEnd := end of SignedData (RFC 5652 section 5.1)
// SignerEnd / InnerEnd for SignerInfo and issuerAndSerialNumber
الوحدة PDFlibCMSRead تمرر الآن تلك النهايات عبر ContentInfo، وغلاف [0] EXPLICIT، وحقول SignedData حتى signerInfos، و SignerIdentifier بصورتيه issuerAndSerialNumber و [0] subjectKeyIdentifier (RFC 5652 §5.3)، وحقول tbsCertificate المقروءة من كل شهادة مضمّنة عند مطابقة الموقّع. وداخل مجموعة الشهادات ومجموعة signerInfos، العنصر الذي يجتاز نهاية المجموعة يوقف الحلقة: تعيد PLExtractCMSCertificates الشهادات التي قبلتها فعلاً ولا تلصق أبداً بايتات crls أو signerInfos التالية على الأخيرة منها. ومطابقة المصدر والرقم التسلسلي تشترط النصفين معاً، لأن الرقم التسلسلي فريد داخل مصدر واحد فقط
لماذا ظهر 2.999.3 على هيئة 1.15.3؟
أول قوسين من OID يندمجان في معرّف فرعي واحد، لا في بايت واحد، وذلك المعرّف الفرعي ينثق بترميز base-128 مثل كل قوس آخر. تعرّفه X.690 §8.19.4 بوصفه 40 * arc1 + arc2؛ وكان DER_OID الأقدم يكتب تلك القيمة عبر Byte(...)، وهي صحيحة حتى 127 فقط، أي قيمة 2.47. وفي 2.999 يكون المجموع 1079، والصب إلى بايت يبقي 55، و55 يفك ترميزه إلى 1.15، فيسمّي المعرّف بهدوء فرعاً آخر من الشجرة. والقيم من 128 إلى 255 تفشل على نحو آخر: تطلق بايتاً واحداً ببت المتابعة مرتفعاً يبتلع القوس التالي. معظم معرفات PKI (1.2.840... و 2.5.29... و 0.4.0...) لا تبلغ الحد قط، ولهذا نجا العيب؛ أما أقواس joint-iso-itu-t من 2.48 فصاعداً تبلغه. DER_OID تخدم المرمّز الخاص بالخصائص الموقّعة والمطابِق في DERFindExtensionByOID وفحص نوع محتوى SignedData دفعة واحدة، فالترميز الخاطئ كان يكسر الكتابة والبحث معاً
uses
SysUtils, PDFlibASN1;
function Hex(const S: AnsiString): string;
var
I: Integer;
begin
Result := '';
for I := 1 to Length(S) do
Result := Result + IntToHex(Byte(S[I]), 2) + ' ';
Result := Trim(Result);
end;
begin
WriteLn(Hex(DER_OID('2.999.3'))); // 06 03 88 37 03
WriteLn(Hex(DER_OID('2.47.1'))); // 06 02 7F 01
WriteLn(Hex(DER_OID('2.48.1'))); // 06 03 81 00 01
WriteLn(Hex(DER_OID('2.5.29.14'))); // 06 03 55 1D 0E
WriteLn(Hex(DER_OID('1.2.840.113549.1.7.2'))); // 06 09 2A 86 48 86 F7 0D 01 07 02
end.
القيمة المدمجة محفوظة في UInt64 عن قصد. تحلل DER_OID الأقواس في Int64، فيمكن للقوس الثاني المشروع أن يبلغ Int64.MaxValue، وإضافة 80 من أجل arc1 = 2 تفيض على عدد صحيح 64-بت مؤشر. تحمل UInt64 Int64.MaxValue + 80 دون لف، والمخزن المؤقت من عشرة بايت يتسع لمجموعات الـ 7 بت العشر التي تحتاجها قيمة 64 بت. ومتجهات الاختبار الجديرة بالاحتفاظ هي التي على جانبي الحد: 2.47 يجب أن تبقى بايتاً واحداً، و 2.48 يجب أن تصبح اثنين
ماذا يضمن تجوال CMS من جهة القراءة؟
يضمن PDFlibCMSRead البنية ولا شيء سواها: يعيد البايتات التي تجلس حيث تقول RFC 5652 إنها يجب أن تجلس، ولا يتحقق من توقيع أو هضم أو فترة صلاحية. التجوال لا يقبل إلا DER، فترفض DERReadTLV الأطوال غير المحددة وأرقام الوسوم متعددة البايتات، و CMS مرمّز بـ BER من موقّع غير ملتزم يبلّغ عن صفر شهادات بدل تخمين جزئي. وشهادات الخصائص (attribute certificates) والبدائل الأخرى في CertificateChoices تُتجاوز لأن لا شيء في المصب يستطيع استعمالها. أما التحقق التشفيري فيبقى مع الكود الذي يملكه، وهو يبدأ بفحوص تغطية البايتات الموصوفة في التوقيع بـ PAdES والتحقق من ByteRange في Delphi ويتواصل مع تصنيف ما تغيّر بعد توقيع PDF
الدرس الأوسع يسري على أي صيغة ثنائية: «داخل المخزن» خاصية أمان ذاكرة، و«داخل الأب» خاصية صواب، والمحلل يحتاج الاثنين. والتفكير نفسه في الأطوال المعادية يسري في تحصين محلل PDF بـ Pascal ضد الملفات الخبيثة. وواجهات استخراج الشهادات وبناء السلاسل والتحقق طويل الأمد التي ناقشتها هذه المقالة تأتي مع losLab PDF Library for Delphi الموجهة إلى Delphi و C++Builder و Lazarus