قبل أن يعرض العارض علامة الصح الخضراء على PDF موقّع، يفعل ثلاثة أشياء ميكانيكية: يقرأ مصفوفة /ByteRange من قاموس التوقيع، ويحسب hash للبعدين البايتين اللذين تصفهما تلك المصفوفة بالضبط، ثم يتحقق من توقيع CMS المخزن، بصيغة hex، في مدخل /Contents الواقع بين هذين البعدين. يكاد كل فشل توقيع إنتاجي يعود إلى واحد من هذه الخطوات الثلاث مرتبا بشكل خاطئ: placeholder أصغر من كتلة التوقيع النهائية، أو hash محسوب على البايتات الخطأ، أو حفظ بعد التوقيع أعاد كتابة بايتات كانت النطاقات تغطيها بالفعل. التشفير نفسه نادرا ما يفشل. محاسبة البايتات هي التي تفشل
يمنح HotPDF تطبيقات Delphi وC++Builder ثلاث طبقات من دعم التوقيع: توقيع PFX باستدعاء واحد، ومسار عمل لموقّع خارجي مخصص لـ HSMs وخدمات التوقيع، وحقول ملف تعريف PAdES مع طوابع زمنية للمستند. تُعرض هنا بهذا الترتيب، لأن كل طبقة موجودة لمعالجة نمط فشل في الطبقة التي قبلها
عقد ByteRange في ISO 32000-1 §12.8
يجب أن يعيش توقيع PDF داخل الملف الذي يوقّعه، وهذا يخلق مشكلة الدجاجة والبيضة: لا يمكن لقيمة التوقيع أن تغطي نفسها. يحل التنسيق ذلك بفتحة. يحجز الكاتب مدخل /Contents ثابت الحجم مملوءا بالأصفار، وتسجل /ByteRange مقطعين: كل شيء قبل الفتحة وكل شيء بعدها. يحسب الموقّع hash لهذين المقطعين، وتكتب بنية CMS الناتجة في الفتحة كقيمة hexadecimal. النتيجة التي يتعثر بها المهندسون: حجم الحجز يتجمد قبل التوقيع، ولذلك يجب أن تتسع الفتحة، التي اختير حجمها سابقا، للتوقيع النهائي بكل شهاداته. نحو 8 KB يكفي لتوقيع CMS detached مع سلسلة شهادات قصيرة
يكشف HotPDF هذا الفرق مباشرة. ينشئ AddSignatureField حقلا مرئيا فارغا ليوقّعه شخص لاحقا في عارض؛ أما AddSignedSignatureField فينشئ الحقل ويحجز فتحة /Contents للإكمال البرمجي. اختيار الاستدعاء الخطأ خطأ كلاسيكي في الأسبوع الأول: الحقل الفارغ لا يعطي الموقّع الخارجي شيئا يملؤه
استدعاء واحد عندما يكون المفتاح في ملف PFX
عندما تكون شهادة التوقيع والمفتاح الخاص داخل ملف PFX/PKCS#12، ينهار خط الأنابيب كله إلى دالة class واحدة:
if THotPDF.SignPDFWithPFX('invoice-unsigned.pdf', 'invoice-signed.pdf',
'company-cert.pfx', 'pfx-password') then
Writeln('Signed: invoice-signed.pdf')
else
raise Exception.Create('PFX signing failed');
الفشل الذي يهيمن على طلبات الدعم هنا لا علاقة له بملف PDF: إنه حاوية PFX نفسها. يقرأ HotPDF ملفات PFX المحمية بـ PBES2، أي اشتقاق مفتاح PBKDF2 مع AES-256-CBC. الحاويات التي تصدرها معالجات شهادات Windows القديمة، أو إصدارات OpenSSL قبل 3.0، تستخدم افتراضيا حماية RC2/3DES الموروثة ولن تُفسر. العلاج هو إعادة تصدير الحاوية مرة واحدة بمعاملات حديثة، ويفعل OpenSSL الحالي ذلك افتراضيا، لا تغيير في الكود. افحص هذا أولا عندما يفشل التوقيع فورا على شهادة "تعمل في كل مكان آخر"
التوقيع الخارجي: احجز، احسب hash، أدرج
يفترض المسار ذو الاستدعاء الواحد أن المفتاح الخاص ملف يستطيع المسار قراءته. مفاتيح التوقيع الإنتاجية لم تعد كذلك غالبا: تكون في HSM، أو USB token، أو خدمة توقيع بعيدة، ولا تستطيع أي مكتبة استدعاءها مباشرة. لهذا التخطيط يقسم HotPDF مسار العمل إلى خطوات على مستوى البايت: اكتب مستند placeholder، احسب نطاقات hash، سلّم إدخال hash إلى الجهة التي تحمل المفتاح، ثم أدخل CMS العائد في مكانه
var
Doc: THotPDF;
Fs: TFileStream;
PdfBytes, HashInput, SigHex: AnsiString;
R1Start, R1Len, R2Start, R2Len, CStart, CLen: Integer;
begin
// 1. Write the document with a reserved /Contents hole
Doc := THotPDF.Create(nil);
try
Doc.FileName := 'placeholder.pdf';
Doc.BeginDoc;
Doc.CurrentPage.AddSignedSignatureField('Sig1',
Rect(50, 100, 350, 150), 8192, 'adbe.pkcs7.detached',
'Contract approval', 'Boston, MA', 'legal@example.com');
Doc.EndDoc;
finally
Doc.Free;
end;
// 2. Load the saved bytes; the returned offsets are 0-based
Fs := TFileStream.Create('placeholder.pdf', fmOpenRead);
try
SetLength(PdfBytes, Fs.Size);
Fs.ReadBuffer(PdfBytes[1], Fs.Size);
finally
Fs.Free;
end;
THotPDF.PreparePDFForSigning(PdfBytes, R1Start, R1Len, R2Start, R2Len,
CStart, CLen);
// 3. Hash both spans and sign externally (HSM, token, service)
HashInput := Copy(PdfBytes, R1Start + 1, R1Len) +
Copy(PdfBytes, R2Start + 1, R2Len);
SigHex := SignWithHsm(HashInput); // your integration: returns CMS as hex
// 4. Splice the signature into the reserved hole
THotPDF.InsertSignatureHex(PdfBytes, SigHex);
Fs := TFileStream.Create('signed.pdf', fmCreate);
try
Fs.WriteBuffer(PdfBytes[1], Length(PdfBytes));
finally
Fs.Free;
end;
end;
قيدان في هذا التسلسل يسببان فشلا إنتاجيا متقطعا عند إغفالهما. أولا، يعمل PreparePDFForSigning على بايتات ملف محفوظ بالكامل؛ يجب أن يُكتب مستند placeholder حتى الاكتمال قبل أن تعني النطاقات أي شيء، فحساب النطاقات على stream قيد الكتابة يعطي offsets لا تطابق التسلسل النهائي. ثانيا، يجب أن يتسع حجز 8192 بايت لـ CMS النهائي. توقيع يضم شهادات وسيطة، أو توقيع تعيده خدمة تضيف signed attributes، قد يتجاوزه، ولا يستطيع InsertSignatureHex تكبير الفتحة. العرض هو مهمة تنجح مع شهادة وتفشل مع أخرى؛ والإصلاح هو إعادة توليد placeholder بحجز أكبر، مقاس من توقيع حقيقي ينتجه الموقّع الفعلي
مستويات PAdES baseline والطوابع الزمنية للمستند
تبني لوائح التوقيع الأوروبية فوق ETSI EN 319 142-1، الذي يعرّف أربعة مستويات PAdES baseline: B-B هو التوقيع الأساسي؛ B-T يضيف طابعا زمنيا موثوقا يثبت وقت إنشائه؛ B-LT يضمّن مادة التحقق، أي الشهادات وبيانات الإبطال، داخل المستند؛ وB-LTA يضيف طوابع زمنية دورية للمستند كي تصمد الأدلة أمام تقادم الخوارزميات. ينشئ HotPDF البنى الخاصة بالمستند لهذه الدورة:
// PAdES baseline signature field (ETSI EN 319 142-1)
Pdf.CurrentPage.AddPAdESSignatureField(
'ApprovalSig', Rect(50, 100, 350, 150), 'B-B',
'Contract approval', 'Boston, MA', 'legal@example.com');
// Document timestamp: larger reservation for the TSA token and chain
Pdf.CurrentPage.AddDocumentTimestampSignature('ArchiveTS', 16384);
لاحظ حجز 16384 بايت للطابع الزمني: يحمل رمز timestamp authority سلسلة شهاداته الخاصة، ولذلك يتجاوز بانتظام 8 KB التي تكفي لتوقيع عادي. الطوابع الزمنية للمستند هي أيضا الآلية خلف صيانة B-LTA، فإعادة ختم أرشيف موقّع كل بضع سنوات بخوارزميات حالية هي ما يبقي توقيع 2026 قابلا للتحقق في 2040
تستحق سلاسل السبب والموقع والاتصال المقبولة من كلا استدعاءي الحقل ملاحظة سياسة: تُخزن كمدخلات قاموس عادية وتُرسم في المظهر المرئي، لكن لا شيء يتحقق منها. إنها توثق القصد للقراء البشر، مثل "Contract approval" ومدينة وصندوق بريد، وسيقرأها المدققون، لذلك املأها باستمرار من بيانات مسار العمل. لكن لا تعامل النص المرئي كدليل أبدا: العبارة التشفيرية تعيش بالكامل في CMS وسلسلة شهاداته، ويتجاهل المدقق المظهر تماما
لماذا يجب أن تنمو الملفات الموقعة فقط
بمجرد وجود توقيع، تتجمد البايتات داخل نطاقاته إلى الأبد. التحديثات التزايدية في ISO 32000-1 §7.5.6 هي الطريقة الشرعية الوحيدة لتغيير الملف بعد ذلك: تضاف الكائنات الجديدة والمعدلة بعد البايتات الأصلية، مع قسم cross-reference جديد يربط إلى السابق. يبقى التوقيع صالحا للمراجعة التي يغطيها، وتعرض العارضات الحالة الصادقة: "المراجعة الموقعة سليمة، والمستند عُدّل بعدها". أما إعادة التسلسل الكاملة فتعيد كتابة النطاقات الموقعة وتدمر التوقيع مباشرة، حتى لو لم يتغير أي عنصر مرئي. آليات الحفظ append-only، ومتى تُضغط، مشروحة في مقالة object streams والتحديثات التزايدية
قيد واحد على مستوى المكتبة يكمل صورة التخطيط: يرفض وضع إخراج PDF/A في HotPDF حقول التوقيع، ولذلك يجب فصل التوافق الأرشيفي والتوقيعات المضمنة في مخرجات منفصلة بدلا من جمعها في ملف واحد. كما أن التوقيع مستقل عن السرية، فهو يثبت المصدر والسلامة لكنه لا يخفي شيئا، وهذا مجال تشفير AES-256 وسياسة الأذونات
يجب أن يكون اختبار قبول خط أنابيب التوقيع مستقلا عن الكود الذي أنتج الملف. افتح الناتج في لوحة التواقيع في Acrobat وتأكد من ثلاث حالات: التوقيع صالح، والهوية ترتبط بالجذر المتوقع، واللوحة لا تبلغ عن تغييرات بعد التوقيع. ثم أفسد بايتا واحدا داخل النطاق الموقّع من نسخة وتأكد أن اللوحة نفسها تبلغ أن المستند قد تغير. خط أنابيب لم يسبق أن شوهد وهو يفشل في التحقق هو خط أنابيب لم يُختبر تحققه فعليا
أسئلة تظهر في مراجعات كود التوقيع
ما حجم حجز /Contents المناسب؟
8192 بايت لتوقيع CMS detached مع سلسلة قصيرة؛ و16384 عندما تدخل الطوابع الزمنية أو الشهادات الوسيطة المضمنة. قس CMS الذي ينتجه الموقّع الحقيقي لديك وأضف هامشا، فالحجز لا يستطيع النمو لاحقا
هل يستطيع مستند واحد حمل توقيعين؟
نعم. يعيش كل توقيع في مراجعته التزايدية الخاصة، وتغطي نطاقات التوقيع الثاني التوقيع الأول، وهذا بالضبط كيف تُبنى مسارات counter-signing
هل يحمي التوقيع محتوى المستند؟
لا. يوفر التوقيع دليلا على السلامة والمصدر؛ ويستطيع أي شخص قراءة الملف. السرية تتطلب تشفيرا مضبوطا بشكل مستقل
تُشحن طبقات التوقيع الثلاث كلها مع HotPDF Component لـ Delphi وC++Builder؛ وتربط صفحة المنتج مرجع signature API الكامل