يرفض HotPDF، مكوّن Delphi PDF، الآن تغليف التواقيع: منذ v2.759.0 تشترط كل من VerifyLoadedSignatureEx والمدقق الدفعي أن تكون الفجوة بين مقطعي /ByteRange هي سلسلة /Contents السداسية بالضبط، والمحددات مشمولة، وتضيف v2.761.0 AddLoadedSignedSignatureField كي يمكن إلحاق توقيع ثانٍ بـ PDF موقّع أصلاً بوصفه مراجعة تزايدية نظيفة. والتغييران يتلازمان، لأن التوقيع الثاني الصحيح هو بالضبط التخطيط الذي يتوقعه المدقق الأصرم
الموقف الذي كشف المشكلة عادي. عقد يوقعه المورّد، ثم يوجَّه إلى معتمد عليه يجب أن يوقّع بالتشارك دون أن يزعج التوقيع الأول. المراجعة الثانية تلحق بعد الأولى، و /ByteRange خاصتها تمتد على الملف الكبير كله، ويجب أن يتحقق التوقيعان. بلوغ ذلك يدوياً كان يعني كتابة قسم تزايدي بنفسك، وملف الاختبار الذي فعل ذلك بالضبط تبيّن أنه بنية تغليف تواقيع نموذجية قبلها المدقق القديم بكل سرور. وإن لم تنظر في واجهة التحقق من قبل فـ دليل التحقق من التواقيع الرقمية لـ PDF بـ HotPDF يغطي الأساسيات التي تبني عليها هذه المقالة
ما الذي يعود للفجوة في ByteRange بالضبط؟
يجب أن تحوي الفجوة قيمة /Contents كاملة ولا شيء سواها: تقول ISO 32000-1 §12.8.3.3 إن السلسلة السداسية، بمحددَيها < و >، تسع تماماً في الفضاء بين نطاقي البايتين، وتحمل ISO 32000-2 §12.8.1 القاعدة نفسها إلى الأمام. والجدول 252 ومستندات PAdES يقولان فقط إن الهضم يستثني قيمة Contents، وهو ما يسهل قراءته استثناءً للأرقام السداسية وحدها. وقرأت إصدارات HotPDF الأقدم ذلك هكذا: هضمت PreparePDFForSigning وإعداد CMS المتدفق قوسَي الزاوية أيضاً، مع تعليق مصدر يصر على وجوب تغطية القوسين. والمدققون الذين يقارنون الفجوة بقيمة التواقيع يوسمون ذلك التخطيط بوصفه نطاق بايتات غير صالح، فنقلت v2.759.0 كلا المحددين خارج النطاقات الموقعة. والفحص المستقل السريع على أي ملف موقّع أن تنظر إلى بايتين: يجب أن يكون البايت عند الإزاحة ByteRange[1] هو < والبايت عند الإزاحة ByteRange[2] - 1 هو >
لماذا يفوت فحص الفجوة غير الفارغة تغليف التواقيع؟
فحص الفجوة غير الفارغة يثبت فقط أن شيئاً ما أُخرج من الهضم، لا ما هو الشيء المُخرج، وهذا هو سطح الهجوم كله. يحجز موضع /Contents آلاف الأرقام الصفرية، بينما حاوية CMS حقيقية نادراً ما تمتلئه. يستطيع مهاجم أن يغلق السلسلة السداسية مبكراً داخل ذلك الحشو الصفري بـ >، ويكتب كائنات جديدة أو مراجعة مزورة في بقية المساحة المحجوزة، ويترك نطاقات البايتات بلا مساس. وما زال توقيع CMS يتحقق لأن كل بايت موقّع لم يتغير، والنطاقات ما زالت تبدأ من 0 وتنتهي عند حجم الملف، وبلّغ مدقق HotPDF القديم svValid مع CoversWholeDocument مضبوطة True. وقارئ PDF في هذه الأثناء يحلل ما جلست في تلك الحفرة غير الموقعة
تعامل HotPDF الآن الفجوة بوصفها بيانات يجب التحقق منها بايتاً ببايت. يقرأ المدقق الفجوة، ويقلع المحددين، ويقبل الأرقام السداسية وفراغات PDF وحدها (جدولة، تغذية سطر، تغذية ورق، إرجاع عربة، فراغ)، ويفك الأرقام ويشترط أن يساوي الناتج قاموس التواقيع /Contents تماماً. وأي شيء آخر يهبط بالنتيجة إلى svInvalidByteRange. ويجري الفحص في مسار التوقيع الأحادي وفي ValidateLoadedSignatureBatch أيضاً، التي احتفظت بمنطق التغطية الخاص بها واحتاجت الإصلاح نفسه. والملفات التي أنتجتها HotPDF قبل v2.759.0، التي حملت فجوتها أرقاماً فقط والقوسين جالسين داخل النطاقات للتو، ما زالت تتحقق، فلا تتلون المستندات المؤرشفة فجأة بالأحمر
var
Pdf: THotPDF;
Info: THPDFSignatureInfo;
Status: THPDFSignatureVerifyStatus;
I: Integer;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.LoadFromFile('SignedTwice.pdf');
for I := 0 to Pdf.GetLoadedSignatureFieldCount - 1 do
begin
Status := Pdf.VerifyLoadedSignatureEx(I, Info);
case Status of
svValid:
if Info.CoversWholeDocument then
Writeln(Info.FieldName, ': valid, covers the whole file')
else
Writeln(Info.FieldName, ': valid, ',
Info.UnsignedTrailingBytes, ' bytes appended later');
svInvalidByteRange:
Writeln(Info.FieldName, ': ByteRange gap rejected (wrapping?)');
else
Writeln(Info.FieldName, ': failed, status ', Ord(Status));
end;
end;
finally
Pdf.Free;
end;
end;
كيف تضيف تواقيعاً ثانية إلى PDF موقّع أصلاً؟
افتح الملف الموقّع بـ BeginIncrementalUpdate، واستدعِ AddLoadedSignedSignatureField، واحفظ بـ SaveIncrementalUpdate، ثم وقّع الملف المعد عبر الدالة الصنفية THotPDF.SignPDFWithPFX. قبل v2.761.0 كانت الوصفة الموثقة باستدعاء THPDFPage.AddSignedSignatureField بعد BeginIncrementalUpdate لا تستطيع العمل، لأن CurrentPage تساوي nil في الوضع التزايدي ولم يستطع شيء تعليق موضع /V على حقل في مستند محمّل. تنشئ الطريقة الجديدة الودجة على الصفحة المحمّلة وتعلق قاموس الموضع نفسه الذي يستخدمه مسار المستند الجديد تحت /V، فيتشارك مسارا التوقيع تسلسلاً واحداً. وللتوقيع الأول نفسه تتناول مقالة إنشاء تواقيع PAdES الرقمية في Delphi خط أنابيب PFX
var
Pdf: THotPDF;
FieldIndex: Integer;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.BeginIncrementalUpdate('Signed.pdf');
// الصفحة 0، مستطيل الودجة بالنقاط، 8192 بايت محجوزة للـ CMS
FieldIndex := Pdf.AddLoadedSignedSignatureField(0, 320, 60, 520, 120,
'ApproverSignature', 8192);
if FieldIndex < 0 then
raise Exception.Create('Page index out of range');
Pdf.SaveIncrementalUpdate('Prepared.pdf');
finally
Pdf.Free;
end;
if not THotPDF.SignPDFWithPFX('Prepared.pdf', 'SignedTwice.pdf',
'approver.pfx', 'pfx-password') then
raise Exception.Create('Second signature failed');
end;
AddLoadedSignedSignatureField أهدأ من أخواتها عن قصد. منشئات الحقول AddLoaded* الأخرى تضبط /NeedAppearances true على الـ AcroForm، الذي يطلب من عارض إعادة توليد مظاهر الحقول؛ وعلى مستند موقّع يمكن لتلك إعادة التوليد أن تعيد كتابة محتوى موقعاً، فتقلع الطريقة الجديدة العلم مجدداً إلا إذا حمله المصدر أصلاً. ويحفظ /SigFlags قيمته الأصلية OR 3 (SignaturesExist زائد AppendOnly، ISO 32000-1 الجدول 219). ولا تحتاج أيضاً إلى استدعاء MarkDirty على الصفحة: الإضافة إلى /Annots و /Fields تنشر علم الاتساخ إلى الكائن غير المباشر المالك، والعلامة الصفحية الصريحة كانت ستسحب قاموس صفحة لم يتغير إلى المراجعة الجديدة، وهو ما يبلّغه تحليل المراجعات بعد ذلك بوصفه تعديل صفحة. وأخيراً تكتب المواضع المؤقتة /ByteRange قبل /Contents، لأن الرقعي يحدد موقع العلامة /ByteRange أولاً ثم يبحث إلى الأمام عن السلسلة السداسية الموافقة
ماذا يتغير حين ينتج CMS موقّع خارجي أو HSM؟
لا يتغير شيء في مسار العمل، لكن الإزاحات صارت تعني ما تقوله المواصفة. تعيد PreparePDFForSigning نطاقين بالترقيم من الصفر فجوتهما سلسلة /Contents كاملة، و ContentsHexStart هو فهرس بترقيم من الواحد لأول رقم سداسي في الـ AnsiString. ويحشى CMS الأقصر بـ 0 في النهاية، قبل > الختامية. ولأن PreparePDFForSigning ترقّع أول علامة غير مرقعة تجدها، جهّز موضعاً مؤقتاً واحداً لكل مراجعة بالضبط، وفضّل InsertSignatureHexAt بالإزاحات المرجعة على InsertSignatureHex القائمة على البحث حين تكون تواقيع أقدم موجودة أصلاً في الملف
var
Bytes, ToSign, CmsHex: AnsiString;
R1Start, R1Len, R2Start, R2Len, HexStart, HexLen: Integer;
begin
Bytes := LoadFileAsAnsiString('Prepared.pdf'); // مساعدتك
if not THotPDF.PreparePDFForSigning(Bytes, R1Start, R1Len,
R2Start, R2Len, HexStart, HexLen) then
raise Exception.Create('No signature placeholder found');
// الفجوة سلسلة hex كاملة: '<' تنهي النطاق 1، و'>' تسبق النطاق 2
Assert(Bytes[R1Start + R1Len + 1] = '<');
Assert(Bytes[R2Start] = '>');
ToSign := Copy(Bytes, R1Start + 1, R1Len) + Copy(Bytes, R2Start + 1, R2Len);
CmsHex := SignDetachedWithHsm(ToSign); // موقّع CMS خاصتك، DER سداسي
if not THotPDF.InsertSignatureHexAt(Bytes, HexStart, HexLen, CmsHex) then
raise Exception.Create('CMS does not fit the reserved space');
SaveAnsiStringToFile(Bytes, 'SignedTwice.pdf'); // مساعدتك
end;
أين حدود الفحوص الجديدة؟
فحص الفجوة يسد حفرة بعينها ولا ينبغي أن يُبالغ في بيعه. ما زال svValid يعني سلامة البايتات زائد مفتاحاً يطابق الشهادة المضمّنة؛ والثقة في تلك الشهادة قرار منفصل. وتتحقق الفجوة فقط حين يملك المدقق بايتات المصدر، التي تقرؤها VerifyLoadedSignatureEx من الملف المحمّل وتأخذها التحميلات الزائدة بـ TStream منك. وللتوقيع الأول في ملف موقّع بالتشارك تكون CoversWholeDocument بالقيمة False، وهل أضافت المراجعة الملحقة توقيعاً فقط أم غيّرت صفحات أيضاً سؤال يعود إلى تحليل DocMDP و FieldMDP والمراجعات في HotPDF. وانتبه أيضاً إلى أن فحص PDF MAC المرفق يقارن الإزاحات بموضعَي < و >، فيقبل التخطيطين القديم والجديد معاً؛ وأي أداة خاصة بك قسرت إزاحات ما قبل v2.759.0 ستفشل أولاً حين تقابل ملفاً موقعاً حديثاً
إن كان تطبيق Delphi أو C++Builder لديك يوقّع أو يوقّع بالتشارك أو يدقق PDFs فالطريق الأسلم أن تترك مكتبة واحدة تنتج التخطيط نفسه وتتحقق منه. و HotPDF، مكوّن Delphi PDF الأصلي يأتي بالتحقق الأصرم من الفجوة والتواقيع الثانية التزايدية وخُطاف الموقّع الخارجي المعروضة أعلاه في مكوّن واحد