يقوم مكون PDFium بتوقيع ملف PDF بتوقيع رقمي PAdES B-B من خلال دالة SignPades الخاصة به: حيث يقوم بتحميل المستند، وتجزئة نطاق البايت الموقع، وبناء بنية CAdES CMS، وإلحاق التوقيع كتحديث متزايد. الخلفية التشفيرية مخصصة لويندوز فقط، لذا احرس كل استدعاء باستخدام PadesCryptoAvailable قبل التوقيع
الموقف مألوف. يهبط ملف PDF الخاص بعقد على مكتبك، وتريد الإدارة القانونية توقيعه رقمياً قبل خروجه، وتصل إلى نفس بناء PDFium الذي تستخدمه بالفعل لعرض المستندات وفحصها، لتكتشف فقط أن PDFium لا يمكنه كتابة توقيع على الإطلاق. واجهة برمجة تطبيقات التوقيع الخاصة به مخصصة للقراءة فقط بشكل صارم. يسد مكون PDFium تلك الفجوة من خلال امتلاك خط أنابيب التوقيع بالكامل في Pascal, بدءاً من دالة التجزئة وحتى الحقن على مستوى البايت، ويمشي هذا المقال في خط الأنابيب هذا من البداية إلى النهاية
لماذا لا يستطيع PDFium كتابة توقيع رقمي؟
يعرض PDFium التواقيع ككائنات للقراءة فقط ولا يقدم شيئاً ينشئ واحداً. تتيح لك عائلة FPDFSignatureObj_* سرد توقيع موجود، وقراءة /Contents الخاصة به، وفحص /ByteRange الخاص به، ولكن لا يوجد مقابل يبني قاموس توقيع، أو يحجز فتحة لـ /Contents، أو يكتب نطاق بايت؛ ويتوفر الحفظ المتزايد (FPDF_SaveAsCopy مع FPDF_INCREMENTAL) ومع ذلك لا يحمل أي خطاف للتوقيع. وبالتالي فإن أي مكون يوقع ملف PDF فوق PDFium يجب أن يولد كل بايت توقيع بنفسه، ولهذا السبب يبني مكون PDFium الآلية من ثلاث وحدات Pascal نقية. يشحن FPC 3.2.2 خوارزميتي md5 و sha1 ولكن لا يشحن SHA-2 على الإطلاق، وواجهة برمجة تطبيقات SHA-256 لـ System.Hash في Delphi غير متوافقة في المصدر مع FPC، لذا فإن FPdfSha256 عبارة عن تطبيق FIPS 180-4 قائم بذاته يحافظ على كل مسار كود CMS في نوع TSHA256Digest واحد بدون تفرع للمترجم. وتقدم FPdfAsn1 مشفر وقارئ DER اللذين تحتاجهما بنيات CMS، وتقوم FPdfCms بتجميع CAdES SignedData فوق كليهما
كيف تقوم بتوقيع ملف PDF رقمياً في Delphi؟
قم بتحميل المستند، ثم استدعى SignPades مع بصمة الشهادة. يقوم مكون PDFium بحل تلك البصمة مقابل مخزن شهادات المستخدم الحالي "MY"، ويسحب الشهادة المطابقة ومفتاحها الخاص، ويكتب نسخة موقعة في المسار الذي تسميه
uses
PDFium, FPdfCrypto;
procedure SignContract(const AThumbprint: string);
var
Pdf: TPdf;
begin
if not PadesCryptoAvailable then
raise Exception.Create('PAdES signing requires the Windows CNG backend');
Pdf := TPdf.Create(nil);
try
Pdf.FileName := 'contract.pdf'; // the document to sign
Pdf.Active := True;
// Second argument: SHA-1 thumbprint of a certificate in the Current
// User "MY" store. First argument: destination for the signed copy.
if not Pdf.SignPades('contract-signed.pdf', AThumbprint) then
raise Exception.Create('Signing failed');
finally
Pdf.Free;
end;
end;
تعد PadesCryptoAvailable الفحص الذي تتحقق منه أولاً، في كل مرة. على ويندوز تعيد True وتكون الخلفية crypt32/ncrypt حية؛ وعلى أي نظام أساسي آخر تعيد False وسيؤدي استدعاء التوقيع إلى إثارة استثناء EPadesCrypto. إن معاملة الحارس كأمر إلزامي يمنع فشل بناء لينكس أو ماك في وقت التشغيل على مسار لا يمكن أن يعمل هناك. البصمة نفسها هي تجزئة SHA-1 للشهادة، وهي نفس القيمة التي يظهرها مدير شهادات ويندوز في علامة تبويب التفاصيل الخاصة به، وتسمي موقعاً معيناً دون وضع مواد المفتاح في التعليمات البرمجية المصدرية لديك أبداً
ما الذي يدخل في CMS: السمات الموقعة ومعيار RFC 5652
توقيع PAdES الأساسي ليس توقيع RSA خام فوق الملف؛ بل هو عبارة عن بنية CAdES CMS SignedData تحمل مجموعة مفروضة من السمات الموقعة، وتصدر دالة FPdfCms.BuildSignedData تلك المجموعة بالضبط: content-type و message-digest و signing-certificate-v2، وهي سمة ESS التي تربط التوقيع بشهادة الموقع عن طريق التجزئة. تفصيل واحد هنا يهزم تقريباً كل تطبيق CMS مكتوب يدوياً. تتطلب الفقرة 5.4 من معيار RFC 5652 حساب خلاصة السمات الموقعة عبر ترميز DER SET OF، العلامة 0x31، بينما تسافر نفس السمات داخل SignerInfo تحت علامة IMPLICIT [0]، 0xA0. يقوم مكون PDFium بترميز مجموعة السمات مرة واحدة، ويجزئ شكل 0x31، ثم يعيد كتابة بايت العلامة البادئ فقط إلى 0xA0 للإرسال، بحيث يخدم مخزن مؤقت واحد كلا الدورين دون تمرير ثانٍ عبر الشجرة
var
Pdf: TPdf;
Opts: TPadesSignOptions;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := 'contract.pdf';
Pdf.Active := True;
Opts := TPadesSignOptions.Default;
Opts.CertificateThumbprint := 'a1b2c3d4e5f6...'; // signer in the MY store
Opts.Reason := 'I approve this agreement';
Opts.Location := 'Berlin, DE';
Opts.ContentsSize := 16384; // hex width of /Contents
if not Pdf.SignPades('contract-signed.pdf', Opts) then
raise Exception.Create('Signing failed');
finally
Pdf.Free;
end;
end;
يضيف الحمل الزائد للخيارات البيانات الوصفية لقاموس التوقيع التي يحددها معيار ISO 32000-1 الفقرة 12.8.1: وهي Reason و Location و ContactInfo و Name، وجميعها اختيارية ومكتوبة بالكامل في قاموس قيمة التوقيع. هناك قيد واحد يسهل التعثر فيه. إذا قمت بتعيين CommitmentTypeOid لإضافة سمة موقعة مؤشر نوع التزام CAdES، فلا تقم أيضاً بتعيين Reason؛ حيث تحظر الفقرة 6.3 من معيار ETSI EN 319 142-1 حمل كليهما، لأن الاثنين يعبران عن نفس القصد بوسائل مختلفة
كيف يتناسب نطاق ByteRange وفتحة /Contents معاً؟
يجب أن يغطي التوقيع الملف بأكمله باستثناء البايتات التي تحمل التوقيع نفسه، ويحل PAdES تلك الدائرية باستخدام عنصر نائب ذي عرض ثابت تديره دالة SignPadesBytes بدقة. حيث تحجز سلسلة /Contents سداسية عشرية بحجم ContentsSize بايت (16384 بشكل افتراضي، وهي أكبر بكثير من CMS SignedData النموذجي)، وتتسلسل التحديث المتزايد لتحديد موقع إزاحة الفتحة الدقيقة، ثم تحسب /ByteRange كفترتين تحصران الفتحة، كل شيء قبل المحدد المفتوح للسلسلة السداسية العشرية وكل شيء بعد محددها المغلق. وتعمل SHA-256 على هاتين الفترتين فقط. ويتم تشفير CMS المنتهي سداسياً عشرياً في الفتحة المحجوزة، وتعبئتها بالأصفار حتى العرض الثابت، ويتم إلحاق تحديث المرجعية الترافقية (cross-reference). ولأن العرض ثابت مقدماً، فإن ملء الفتحة لا يزيح أي بايت لاحق، وهو السبب الكامل لبقاء نطاق البايت صالحاً؛ ويتم الاحتفاظ ببايتات المستند الأصلية حرفياً، بحيث ينجو التوقيع السابق على نفس الملف سليماً تماماً كما يتطلب التوقيع المتزايد لمعيار ISO 32000-1 الفقرة 12.8.1
خلفية Windows CNG وحدودها
يوقع مكون PDFium على نظام التشغيل ويندوز فقط، وهذا الحد متعمد. يربط FPdfCryptoWin ملفي crypt32.dll و ncrypt.dll ديناميكياً، مما لا يضيف أي تبعية لملفات DLL في وقت التجميع، وسلسلة التوقيع هي CNG قياسية: افتح مخزن MY، وابحث عن الشهادة عن طريق التجزئة، واحصل على مقبض مفتاحها الخاص من خلال CryptAcquireCertificatePrivateKey، واستدعى NCryptSignHash. يتم دعم كل من RSA مع PKCS#1 v1.5 و RSA-PSS و ECDSA. وتحتاج ECDSA إلى إصلاح واحد لا تحتاجه الأخريات، لأن NCryptSignHash تعيد زوج r-and-s الخام لمعيار IEEE P1363 بينما تتوقع CMS تسلسل DER ECDSA-Sig-Value SEQUENCE، لذا تعيد الخلفية ترميزه وفقاً لمعيار RFC 5480
var
Pdf: TPdf;
Opts: TPadesSignOptions;
Output: TFileStream;
begin
if not PadesCryptoAvailable then
Exit; // no signing backend on this platform
Opts := TPadesSignOptions.Default;
Opts.CertificateThumbprint := ReadThumbprintFromConfig;
Pdf := TPdf.Create(nil);
Output := TFileStream.Create('contract-signed.pdf', fmCreate);
try
Pdf.FileName := 'contract.pdf';
Pdf.Active := True;
Pdf.SignPadesToStream(Output, Opts);
finally
Output.Free;
Pdf.Free;
end;
end;
النتيجة العملية هي أن المفتاح الخاص يجب أن يعيش في مخزن شهادات ويندوز. وتعمل الشهادة المحفوظة في ملف PFX فقط بعد استيرادها إلى مخزن المستخدم الحالي، وعند هذه النقطة تكون بصمتها هي القيمة التي تمررها إلى SignPades. لا يحتوي هذا الإصدار على مسار PKCS#11 أو HSM ولا على خلفية ملف مفتاح برمجيات، لذا عندما تعيد PadesCryptoAvailable القيمة False، لا يتوفر توقيع ببساطة على هذا الجهاز
أين يتوقف PAdES B-B
يعد PAdES B-B هو الأساس، والحد الأدنى لمستويات PAdES الأربعة: فهو يثبت من وقع وأن البايتات لم تتغير منذ ذلك الحين، ولا شيء أبعد من ذلك. لا يحمل توقيع B-B طابعاً زمنياً موثوقاً به، وبالتالي لا يمكنه إثبات متى حدث التوقيع، ولا يضمن أي بيانات إلغاء، وبالتالي يجب على المدقق بعد سنوات جلب سلسلة الشهادات وحالتها بنفسه. هذه الفجوات هي بالضبط ما تغلقه المستويات الأعلى. عندما تحتاج إلى وقت توقيع يقبله المراجع، فإن إضافة طابع زمني RFC 3161 و DSS للتحقق طويل الأجل ينقل التوقيع إلى B-T وما فوقه؛ وعندما تريد قراءة توقيع منتهٍ والتأكد من المستوى الذي وصل إليه، فإن فحص توقيع PDF ومستوى PAdES الخاص به هو الأداة المصاحبة؛ وقبل توقيع أي شيء على الإطلاق، يخبرك تدقيق ملف PDF بحثاً عن المخاطر الأمنية بما أنت على وشك وضع اسمك عليه
إن دالات SignPades المعروضة هنا تشحن مع مكون PDFium لـ Delphi و C++Builder، جنباً إلى جنب مع فحص التواقيع للقراءة فقط الذي يقدمه PDFium خارج الصندوق