توقيع PDF هو في الغالب محاسبة بايت (byte accounting)، ومحاسبة البايت هي حيث تسير الأمور بشكل خاطئ. يعمل التشفير على تعليمات برمجية تم تدقيقها لعقدين من الزمن، وهذا الجزء لا يفشل تقريباً أبداً. ما يفشل في الإنتاج هو أكثر تواضعاً: عنصر نائب (placeholder) تم حجزه صغيراً جداً للتوقيع الحقيقي، أو تجزئة (hash) تم أخذها على امتداد خاطئ من الملف، أو "حفظ" بعد التوقيع أعاد كتابة البايتات بهدوء والتي كان التوقيع قد جمدها بالفعل. ضع البايتات بشكل صحيح وستهتم علامة الاختيار الخضراء بنفسها
يغطي HotPDF التوقيع لـ Delphi و C++Builder على ثلاثة مستويات، وتختار بينها من خلال الإجابة على سؤال واحد: أين يعيش المفتاح الخاص (private key)؟ يحتاج ملف PFX على القرص إلى استدعاء دالة واحد. يحتاج المفتاح المقفل في HSM أو خدمة توقيع عن بُعد إلى تسلسل reserve-hash-insert (حجز-تجزئة-إدراج)، لأنه لا يمكن لأي مكتبة الوصول إلى رمز (token) وسحب المفتاح منه. يحتاج التوقيع الذي يجب أن يلبي اللوائح الأوروبية إلى هياكل PAdES الأساسية (baseline) بالإضافة إلى ذلك. تتبع الأقسام أدناه هذا التقدم
كيف يحدد /ByteRange البايتات الموقعة
يجب أن يعيش التوقيع داخل الملف الذي يوقعه، ولا يمكنه توقيع نفسه. يتغلب PDF على هذه المفارقة عن طريق ترك ثقب (hole). قبل التوقيع، يحجز الكاتب إدخال /Contents بحجم ثابت مليء بالأصفار ويسجل مصفوفة /ByteRange للامتدادين (spans) على جانبيها: كل شيء قبل الثقب، وكل شيء بعده. يقوم الموقع بتجزئة هذين الامتدادين ويكتب كتلة CMS الناتجة في الثقب كنظام سداسي عشري (hexadecimal). الفخ يكمن في كلمة ثابت (fixed). أنت تلتزم بحجم هذا الثقب قبل أن تعرف حجم التوقيع النهائي، لذا يجب أن يكون الحجز تقديراً مبالغاً فيه بثقة. ثمانية كيلوبايت تتسع بشكل مريح لتوقيع CMS منفصل مع سلسلة شهادات قصيرة
يقسم HotPDF الحالتين إلى استدعاءين، والخلط بينهما خطأ مبكر شائع. يضع AddSignatureField حقلاً فارغاً ومرئياً ليوقعه شخص لاحقاً في العارض. يقوم AddSignedSignatureField بإنشاء الحقل ويحجز ثقب /Contents، وهو ما تريده كلما كان الكود، بدلاً من الإنسان، هو الذي سيكمل التوقيع. سلم للموقع الخارجي حقلاً فارغاً ولن يكون لديه شيء لملئه
مسار الاستدعاء الواحد: التوقيع من PFX
عندما توجد الشهادة ومفتاحها الخاص في ملف PFX/PKCS#12 يمكن لعمليتك قراءته، ينخفض خط الأنابيب بأكمله (pipeline) إلى دالة صنف (class function):
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 الحاويات المحمية بـ PBES2، مما يعني اشتقاق مفتاح PBKDF2 عبر AES-256-CBC. عادةً ما يتم تغليف ملف PFX المُصدَّر بواسطة معالج شهادات Windows الأقدم، أو بواسطة OpenSSL قبل الإصدار 3.0، في RC2 القديم أو 3DES بدلاً من ذلك، ولن يتم تحليله ببساطة. يتمثل الإصلاح في إعادة تصدير الحاوية مرة واحدة مع حماية حديثة؛ يقوم OpenSSL اليوم بذلك بشكل افتراضي، وهو ليس تغييراً في التعليمات البرمجية. لذلك عندما يموت التوقيع على الفور في شهادة "تعمل في كل مكان آخر"، انظر في كيفية إنشاء PFX قبل أن تشك في التعليمات البرمجية الخاصة بك
مسار reserve-hash-insert (الحجز-التجزئة-الإدراج) لوحدات HSM والرموز (tokens)
يفترض مسار الاستدعاء الواحد أن عمليتك يمكنها قراءة المفتاح كملف. وبشكل متزايد، لا يمكن ذلك. يجلس المفتاح في HSM، أو على رمز USB (USB token)، أو خلف واجهة برمجة تطبيقات (API) لخدمة توقيع، ولا توجد طريقة لوصول مكتبة إليه مباشرة. يعالج HotPDF ذلك عن طريق تقسيم التوقيع إلى خطوات على مستوى البايت: اكتب مستنداً نائباً، واطلب من المكتبة نطاقات التجزئة، وقم بتمرير إدخال التجزئة إلى كل ما يحمل المفتاح، ثم اربط 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 تعمل على بايتات ملف مكتمل. يجب كتابة العنصر النائب وحفظه بالكامل قبل أن تعني الإزاحات (offsets) أي شيء؛ احسبها مقابل تيار (stream) لا يزال قيد التجميع ولن تتوافق مع البايتات التي ستقوم بتجزئتها في النهاية. الثانية هي حجم الحجز، مرة أخرى. يجب أن تستوعب الـ 8192 بايت التي طلبتها CMS النهائي، والتوقيع الذي يحمل شهادات وسيطة، أو التوقيع الذي تزينه الخدمة بسمات موقعة، يمكن أن يتجاوز ذلك. لن يوسع InsertSignatureHex الثقب لتوفير مساحة. الدليل هو خط أنابيب يوقع جيداً بشهادة واحدة ويفشل مع الشهادة التالية؛ العلاج هو إعادة إنشاء العنصر النائب بحجز يتم قياسه من توقيع حقيقي ينتجه الموقع الفعلي، وليس تخمينه
أساسيات PAdES، والطوابع الزمنية التي تبقي التوقيع حياً
إذا كنت تقوم بالتوقيع بموجب القواعد الأوروبية، فإن المعيار المطبق هو ETSI EN 319 142-1، والذي يكدس أربعة مستويات أساسية (baseline levels) لـ PAdES. B-B هو التوقيع العادي. يضيف B-T طابعاً زمنياً موثوقاً (trusted timestamp) يثبت متى تم إجراؤه. يضمن B-LT مواد التحقق (validation material)، والشهادات وبيانات الإلغاء، داخل المستند حتى يظل من الممكن التحقق منه بعد سنوات. يضع B-LTA طبقات من الطوابع الزمنية الدورية للمستند (periodic document timestamps) في الأعلى، بحيث يتجاوز الدليل عمر الخوارزميات التي بني عليها. يصدر 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) رمزاً (token) يسحب سلسلة الشهادات الخاصة به معها، لذلك فهو يحتاج بشكل روتيني إلى مساحة أكبر من 8 كيلوبايت التي يكتفي بها التوقيع العادي. هذه الطوابع الزمنية للمستند هي أيضاً الآلية وراء B-LTA: إعادة الطابع الزمني لتوقيع مؤرشف كل بضع سنوات، باستخدام خوارزميات لا تزال حديثة، هو ما يحافظ على إمكانية التحقق من مستند وقعته في عام 2026 في عام 2040
كلمة حول سلاسل السبب (reason)، والموقع (location)، وجهة الاتصال (contact) التي يقبلها كلا استدعاءي الحقل: إنها بيانات وصفية للراحة (convenience metadata) ولا شيء أكثر من ذلك. يخزنها HotPDF كإدخالات قاموس عادية ويرسمها في مظهر التوقيع المرئي، لكن لا يتحقق منها أي مدقق (validator) مقابل أي شيء. قم بملئها باستمرار من بيانات سير العمل الخاصة بك، حيث يقرؤها المدققون، ومن ثم لا تخطئ أبداً في اعتبارها دليلاً. تعيش المطالبة التشفيرية (cryptographic claim) الفعلية بالكامل في CMS وسلسلة شهاداته، ويتجاهل المدقق (verifier) النص المرئي تماماً
بعد التوقيع، قد ينمو الملف فقط
في اللحظة التي يوجد فيها توقيع، يتم تجميد البايتات داخل نطاقاته. الطريقة المشروعة الوحيدة لتغيير الملف بعد ذلك هي التحديث التزايدي (incremental update) وفقاً لـ ISO 32000-1 §7.5.6، والذي يلحق الكائنات الجديدة والمعدلة بعد البايتات الأصلية ويربط قسماً جديداً من المراجع المتقاطعة (cross-reference) بها. عند القيام بذلك بهذه الطريقة، يظل التوقيع صالحاً لمراجعته ويبلغ العارض بالحالة الصادقة: المراجعة الموقعة سليمة، وتم توسيع المستند بعد ذلك. بدلاً من ذلك، أعد تسلسل الملف بأكمله وستعيد كتابة الامتدادات الموقعة، مما يدمر التوقيع حتى في حالة عدم تغير أي شيء مرئي. آلية المراجعة نفسها هي أيضاً كيفية حمل مستند واحد لعدة توقيعات: يهبط كل توقيع جديد في التحديث التزايدي الخاص به، وتغطي نطاقاته كل شيء قبله، بما في ذلك التوقيعات السابقة. تتم تغطية آليات الإلحاق فقط (append-only mechanics)، ومتى يكون ضغطها آمناً، في المقال حول تيارات الكائنات والتحديثات التزايدية
هناك حدودان يستحقان وضعهما في الاعتبار أثناء التصميم. يرفض وضع إخراج PDF/A الخاص بـ HotPDF حقول التوقيع بشكل صريح، لذا يجب أن يتم شحن مطابقة الأرشيف (archival conformance) والتوقيع المضمن كملفات منفصلة. والتوقيع لا يقول شيئاً عن السرية: إنه يثبت من أنتج مستنداً وأنه لم يتغير منذ ذلك الحين، ولكن لا يزال بإمكان أي شخص قراءته. إخفاء المحتويات هو عمل منفصل، يتم التعامل معه من خلال تشفير AES-256 وسياسة الأذونات
مهما كان ما تبنيه، اختبره بشيء آخر غير الكود الذي كتب الملف. افتح الإخراج في لوحة توقيع Acrobat وأكد ثلاثة أشياء: التوقيع صالح، والسلاسل هوياتية (identity chains) إلى الجذر الذي توقعته، وتفيد اللوحة بعدم وجود تغييرات منذ التوقيع. ثم اقلب بايت واحد داخل النطاق الموقع لنسخة يمكن التخلص منها وتأكد من أن اللوحة تسمي الآن المستند معدلاً. خط أنابيب التوقيع الذي لم تشاهده أبداً يرفض ملفاً تم العبث به هو خط لم يتم اختبار التحقق الخاص به حقاً
يتم شحن مستويات التوقيع الثلاثة جميعها مع مكون HotPDF لـ Delphi و C++Builder؛ تربط صفحة المنتج مرجع واجهة برمجة تطبيقات التوقيع الكامل