مقال تقني

توقيع PAdES عن بُعد في PDFium VCL: مفاتيح HSM والسحابة

تقسم PDFiumPas توقيع PAdES إلى استدعاءين بحيث لا يكون على المفتاح الخاص أن يوجد في عمليتك أبداً. تكتب PreparePadesRemoteSignature تحديثاً تزايدياً بحيز /Contents فارغ ثابت العرض، وتُعيد سجل طلب يحمل ملخص المستند بـ SHA-256، وByteRange الدقيق، وبصمة للملف المُحضَّر. تأخذ CompletePadesRemoteSignature ملف CMS المنفصل الذي تُعيده خدمة التوقيع لديك وتضعه في تلك الفتحة المحجوزة

بين هذين الاستدعاءين، يمكن أن تمر دقائق أو ساعات، ويمكن أن تعيد العملية التشغيل، ويمكن أن ينتقل العمل إلى جهاز آخر. تلك الفجوة هي السبب الكامل وراء تشكيل واجهة البرمجة بهذه الطريقة

لماذا لا يستطيع مفتاح عن بُعد استخدام استدعاء التوقيع العادي؟

لأن SignPadesBytes تفترض أن عملية التوقيع تحدث داخل الاستدعاء. فهي تبني التحديث التزايدي، وتحسب الملخص على ByteRange، وتوقّعه، وتكتب النتيجة، كل ذلك قبل العودة. هذا صحيح تماماً عندما يعيش المفتاح في مخزن شهادات Windows أو في ملف PKCS#12 قمت بتحميله

هذا مستحيل عندما يعيش المفتاح في HSM شبكي، أو جهاز إنشاء توقيع مؤهَّل يشغّله مزود خدمة ثقة، أو واجهة برمجة توقيع سحابية تتطلب من المستخدم التأكيد على هاتف. في تلك الحالات لا يكون التسلسل استدعاء دالة، بل محادثة: ترسل ملخصاً، ويوثِّق شيء آخر إنساناً، ويعود CMS لاحقاً. لا تستطيع واجهة برمجة متزامنة التعبير عن «لاحقاً» دون حجب مسار تنفيذ على عملية قد تحتاج عاملاً ثانياً

بروتوكول المرحلتين

المرحلة الأولى تُحضِّر المستند. تُلحق PDFiumPas حقل التوقيع وقاموس القيمة، وتحجز ContentsSize بايت من المساحة المُرمَّزة بالسداسي عشري في /Contents، وتحسب ByteRange حول ذلك الحجز، وتُنتج TPadesRemoteSigningRequest يحتوي على FormatVersion وPreparedFingerprint وDocumentDigest وByteRange ذي العناصر الأربعة، وContentsHexOffset وContentsSize

القيمة الوحيدة التي تحتاجها خدمة التوقيع لديك هي DocumentDigest: ملخص SHA-256 الذي يجب أن يحمله SignedData الخاص بـ CAdES المُعاد كملخص رسالته. كل شيء آخر في السجل موجود لكي تستطيع المرحلة الثانية إثبات أن الملف الذي تُكمله هو الملف الذي حُسب منه ذلك الملخص

uses
  FPdfPades;

var
  Options: TPadesRemoteSignOptions;
  Request: TPadesRemoteSigningRequest;
  Source, Prepared, Session: TFileStream;
begin
  Options := TPadesRemoteSignOptions.Default;
  Options.Reason := 'Approved by finance';
  Options.Location := 'Lisbon';
  Options.Name := 'A. Moreira';
  Options.SigningTimeUtc := NowUtc;
  Options.ContentsSize := 16384;   // بايتات سداسية عشرية محجوزة لـ CMS

  Source := TFileStream.Create('contract.pdf', fmOpenRead or fmShareDenyWrite);
  Prepared := TFileStream.Create('contract.prepared.pdf', fmCreate);
  try
    PreparePadesRemoteSignature(Source, Prepared, Options, Request);
  finally
    Prepared.Free;
    Source.Free;
  end;

  // احفظ الجلسة بحيث يستطيع تشغيل لاحق - أو جهاز آخر - إنهاءها
  Session := TFileStream.Create('contract.signreq', fmCreate);
  try
    SavePadesRemoteSigningRequest(Session, Request);
  finally
    Session.Free;
  end;

  SendDigestToSigningService(Request.DocumentDigest);
end;

ماذا ترفض Complete، ولماذا يوجد كل فحص؟

الإكمال هو حيث يخطئ تصميم التوقيع عن بُعد عادة، لذا فإن التحقق صارم عمداً بلا رحمة. ترفض CompletePadesRemoteSignature ملف PDF مُحضَّراً لم تعد بصمته تطابق الطلب، وByteRange لا يطابق إحداثيات الحيز المحجوز المسجَّلة، ومحدِّدات /Contents مُعدَّلة، وحيزاً لم يعد فارغاً، وCMS أكبر من الحجز، وCMS ليس قيمة DER واحدة بالضبط، وشكل SignedData غير مدعوم، وسمة signing-certificate-v2 مفقودة، وCMS لا يساوي ملخص رسالته ملخص المستند المُحضَّر

يقابل كل واحد من هذه فشلاً حقيقياً. يلتقط فحصا البصمة وByteRange الحالة التي يعيد فيها شخص ما توليد الملف المُحضَّر بين المرحلتين، وهو ما كان سينتج توقيعاً يتحقق مقابل بايتات لا يملكها أحد. يلتقط فحص الحيز الفارغ الإكمال المزدوج، حيث يُكتب CMS ثانٍ فوق توقيع موجود بالفعل. يلتقط فحص ملخص الرسالة الحالة الأخطر على الإطلاق: CMS مُشكَّل بشكل صحيح لكنه موقَّع على مستند مختلف، وهو ما تحصل عليه عندما يخلط طابور بين جلستي توقيع متزامنتين. بدونه ستنتج ملفاً يبدو موقَّعاً ويفشل في التحقق في كل مكان، أو أسوأ، يحمل موافقة شخص آخر

متطلب signing-certificate-v2 مسألة مطابقة PAdES أكثر منه مسألة تكامل. تتطلب ETSI EN 319 142 ربط شهادة التوقيع داخل السمات الموقَّعة، وCMS يفتقر إلى تلك السمة ليس توقيع PAdES حتى لو تحقق تشفيرياً. رفضه عند الإكمال يعني أنك تكتشف ذلك هنا، لا في تقرير أداة تحقق من عميل، وهو موضوع مستكشَف بمزيد من التفصيل في لماذا ترفض أدوات التحقق توقيعات PAdES

var
  Request: TPadesRemoteSigningRequest;
  Session, Prepared, Dest: TFileStream;
  CmsDer: TBytes;
begin
  Session := TFileStream.Create('contract.signreq', fmOpenRead);
  try
    Request := LoadPadesRemoteSigningRequest(Session);
  finally
    Session.Free;
  end;

  CmsDer := FetchDetachedCmsFromService;   // مُعادة من HSM أو TSP

  Prepared := TFileStream.Create('contract.prepared.pdf', fmOpenRead);
  Dest := TFileStream.Create('contract.signed.pdf', fmCreate);
  try
    try
      CompletePadesRemoteSignature(Prepared, Dest, Request, CmsDer);
    except
      on E: EPadesCrypto do
        // كل رفض يحمل سبباً محدداً؛ سجّله حرفياً
        FailSession(E.Message);
    end;
  finally
    Dest.Free;
    Prepared.Free;
  end;
end;

عبور حدود العمليات والأجهزة

تُسلسِل SavePadesRemoteSigningRequest وLoadPadesRemoteSigningRequest الجلسة عبر صيغة ثنائية مستقرة ومُرقَّمة بإصدار، وهذا ما يجعل التصميم عملياً لا صحيحاً فقط. يستطيع تطبيق ويب تحضير مستند في طلب واحد، وتخزين ملف PDF المُحضَّر وكتلة الجلسة، وإعادة ملخص إلى المتصفح لتوقيع بطاقة ذكية، وإكمال الملف في معالج طلب مختلف تماماً

حقل FormatVersion هو ما يبقي ذلك آمناً عبر الترقيات. جلسة كُتبت بنسخة أقدم وحُمِّلت بنسخة أحدث تُعرَف أو تُرفض صراحة، بدلاً من إساءة قراءتها كسجل بشكل مختلف. إذا كان طابورك يستطيع حمل جلسات لأيام، عامل إصدار الصيغة كحقيقة تشغيلية تستحق التسجيل، لا تفصيلاً في التنفيذ

تحديد حجم الحيز المحجوز

ContentsSize هو المعامل الوحيد الذي يجب أن تفكر فيه، لأنه ثابت قبل وجود CMS. إنه يحسب الحجز المُرمَّز بالسداسي عشري، بحيث يحتاج CMS من نوع DER بحجم 6 كيلوبايت إلى 12 كيلوبايت من المساحة على الأقل، ويحدّ التنفيذ الحجز عند 64 ميبيبايت

احجز قليلاً جداً وسيفشل الإكمال بخطأ CMS كبير الحجم بعد أن تكون خدمة التوقيع لديك قد أنجزت عملها بالفعل، وهو ما يعني في خدمة توقيع مؤهَّلة مُقاسة عملية مهدورة. احجز كثيراً جداً وسيحمل كل مستند موقَّع الحشو إلى الأبد. النهج المعقول هو القياس: وقّع مستنداً واحداً بسلسلة شهاداتك الحقيقية، وانظر إلى طول DER، وضاعفه للسداسي عشري، ثم أضف هامشاً سخياً لرمز الطابع الزمني إن كنت تنوي الترقية إلى توقيع من مستوى T. تنمو السلاسل ذات الوسطاء المتعددين واستجابة OCSP الطويلة أسرع مما يتوقع الناس

ما الذي يأتي بعد التوقيع

التوقيع عن بُعد المكتمل هو PAdES B-B. يحتاج التحقق طويل الأمد إلى طابع زمني ومواد تحقق، وهو تحديث تزايدي منفصل يضيف DSS وقواميس VRI الخاصة بكل توقيع، موضح في التوقيعات طويلة الأمد مع طوابع RFC 3161 الزمنية وDSS. هذه الخطوة محلية: فهي تضيف شهادات، واستجابات OCSP، وCRL، لا يحتاج أي منها إلى المفتاح الخاص

قبل الشحن، تحقق مما أنتجته بمسار الكود نفسه الذي سيستخدمه طرف معتمِد، موضح في فحص التوقيعات الرقمية ومستويات PAdES. التوقيع والتحقق كودان مختلفان، وخط أنابيب التوقيع عن بُعد هو بالضبط المكان الذي يمكن أن يتباعد فيه الاثنان دون أن يلاحظ أحد حتى تقول أداة تحقق خارجية ذلك

PDFiumPas مكون لـ Delphi وLazarus مبني حول محرك PDFium مع حزمة PAdES أصلية بلغة Pascal، بحيث يعمل التوقيع والختم الزمني والتحقق دون أدوات سطر أوامر خارجية. التوثيق الكامل لواجهة البرمجة ونسخة تجريبية متاحان على صفحة مكون PDFium لـ Delphi