تخطَّ إلى المحتوى
getnextpdf.com

التوقيع على نطاق واسع، دون تنازل

Spec: ISO 32000-2, §12.8Spec: ETSI EN 319 142-1Spec: RFC 5652, §5.1

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

التوقيع حقيقة خاصّة بكل مستند. يُحسَب ملخّصه على نطاق بايتات مُعلَن يتخطّى قيمة التوقيع نفسها (Spec: ISO 32000-2, §12.8)، فلا توجد طريقة صادقة لتوقيع ألف مستند “كدفعة” بضربة واحدة — إذ يحمل كلٌّ منها كائن ⁨CMS SignedData⁩ خاصًّا به على بايتاته الخاصّة (Spec: RFC 5652, §5.1). لذا يضاعف الحجم فرص أن يسير شيء واحد بالضبط على نحو خاطئ بصمت: مَقبِض مفتاح أخفق لوهلة، أو سلطة طوابع زمنية انتهت مهلتها، أو عامل مات وهو يحمل ملفًا مكتوبًا نصفه.

النتيجة الباهظة ليست انهيارًا. الانهيار صاخب وتعيد المحاولة عليه. أما النتيجة الباهظة فهي نتيجة صامتة — ملف ⁨PDF⁩ غير موقَّع يبدو منجَزًا قابعًا في أرشيف، يكتشفه بعد أشهر مدقّق تابع لمراجِع. على نطاق واسع، يتعذّر تمييز “موقَّع في معظمه” عن “موقَّع” إلى أن يُفحَص الملف المهمّ. والغاية كلّها من التوقيع على نطاق واسع جعل تلك النتيجة مستحيلة بنيويًّا، لا نادرة إحصائيًّا.

  • كل مستند يُوقَّع منفردًا، على نطاق بايتاته الخاصّ. الدفعة كلمة جدولة، لا كلمة تشفيرية. لا يوجد توقيع مشترك.
  • المستوى عقد، لا تلميح. تُسمّي مستوى خط أساس ⁨PAdES⁩ وينتج المحرّك ذلك المستوى بالضبط لكل مستند، أو يُخفق ذلك المستند بصوت عالٍ (Spec: ETSI EN 319 142-1).
  • خطّ المعالجة يفشل مغلقًا. المستند الذي يتعذّر توقيعه بصورة صحيحة لا يمرّ بوصفه بايتات عادية. بل يُحتجَز، ولا يُمرَّر.
  • التزامن خاصّ بكل مستند، وآمن بالبناء. وحدات التوقيع لا تتشارك حالة قابلة للتغيير، فلا يستطيع عاملان إفساد مخرج أحدهما الآخر.
  • التشغيلات الكبيرة دائمة. المخرَج المُلتزَم لا يُعاد إصداره عند الاستئناف؛ والتشغيل المنهار يواصل من آخر نقطة تفتيش بدلًا من إعادة توقيع كل شيء.

يقوم التصميم على فصل واحد: إنتاج التوقيع خطوة صغيرة حتمية خاصّة بكل مستند؛ وتشغيل آلاف منها بأمان خطوة تنسيق. والإبقاء على هاتين منفصلتين هو ما يجعل كلًّا منهما تبقى بسيطة.

خطوة التوقيع هي التي يجب ألا تتنازل أبدًا. تطلب مستوى — حالة تعداد SignatureLevel، لا سلسلة نصية على المحرّك تفسيرها — ويُعامَل ذلك المستوى بوصفه عقدًا لـذلك المستند. ينتج المحرّك المستوى المطلوب أو يتوقّف بخطأ قابل للمعالجة؛ ولا يوقّع بصمت عند مستوى أدنى ثم يدع سجلًّا يدّعي مستوى أعلى. والصحّة لا تتراخى لأن وراء هذا المستند مستندات أكثر. فالتوقيع المئة ألف يُحسَب بالعناية نفسها التي يُحسَب بها الأول تمامًا.

قاعدة الفشل المغلق هي ما يجعل ذلك جديرًا بالثقة على نطاق واسع. يرفض مسار التوقيع في ⁨NextPDF⁩ إصدار قطعة تبدو معقولة لكنها غير موقَّعة بدلًا من القطعة التي طلبتها. والمسار التطبيقي المدعوم هو واجهة ⁨Document⁩ البرمجية عالية المستوى: تهيّئ التوقيع بـDocument::setSignature() ثم تطلب البايتات بـDocument::getPdfData() (أو save() / output())، وذلك المرور الكتابي الواحد إمّا يُصدِر ملف ⁨PDF⁩ موقَّعًا بصورة صحيحة أو يطرح قبل إعادة البايتات — لا ملفًا غير موقَّع يظنّه المستدعي موقَّعًا. ومطبَّقة عبر دفعة، هذه هي القاعدة التي تحوّل “أفلت أحدها غير موقَّع” من عيب كامن صامت إلى مهمّة واحدة مُخفِقة قابلة لإعادة المحاولة.

  1. Warm the signing material onceعند إقلاع العامل، افتح مصدر المفتاح/الشهادة وعميل الطابع الزمني. تُدفَع هذه الكلفة مرّة واحدة لكل عامل، لا مرّة لكل مستند.
  2. Enqueue the documentsيحتجز طابور المهامّ الخاصّة بكل مستند. الطابور هو مِقبَض الإنتاجية — يتوسّع عمّال التوقيع أفقيًّا خلفه.
  3. Render and sign one documentتعرض وحدة قابلة للتخلّص المستند، ثم توقّعه على نطاق بايتاته الخاصّ عند مستوى PAdES المطلوب. لا شيء يُتشارَك مع المستند التالي.
  4. Commit on success, hold on failureيُلتزَم الملف الموقَّع بصورة صحيحة مرّة واحدة. أما المستند الذي تعذّر توقيعه فيُخفَق ويُعاد — ولا يُصدَر قطّ بوصفه بايتات غير موقَّعة.
  5. Checkpoint, and resume on crashيسجّل التشغيل الدائم ما الذي التُزِم. وبعد الانهيار يواصل من آخر نقطة تفتيش بدلًا من إعادة توقيع الدفعة بأكملها.
تشغيل توقيع عالي الحجم من طرف إلى طرف: تُسخَّن مادة التوقيع المشتركة مرّة واحدة؛ ويُعرَض كل مستند ويُوقَّع منفردًا على وحدة قابلة للتخلّص؛ وتُلتزَم النتيجة الموقَّعة بصورة صحيحة مرّة واحدة بالضبط، بينما يُحتجَز أيّ إخفاق لإعادة المحاولة ولا يُمرَّر قطّ بوصفه بايتات غير موقَّعة؛ ويستأنف التشغيل المنهار من نقطة تفتيشه.

تمنحك ⁨Core⁩ الصحّة التشفيرية: توقيع ⁨CMS⁩ برمجيًّا و⁨PAdES⁩ ⁨B-B⁩ (مع ⁨B-T⁩ عبر عميل الطابع الزمني) حيث يُوقَّع كل مستند منفردًا ويفشل مغلقًا. أما التنسيق الذي يجعل التشغيل الكبير دائمًا ومتزامنًا ومرّة واحدة بالضبط — محرّك العرض الخالي من الأثر الجانبي إضافة إلى المُلتزِم، ومخازن نقاط التفتيش والتعطيل المتكرّر والرسائل المهملة — فهو وحدة ⁨Stream⁩ في الإصدارات المتقدّمة؛ والتوقيع المدعوم بالعتاد عبر ⁨HSM⁩ أو ⁨KMS⁩ سحابي كذلك حدّ من حدود الإصدارات المتقدّمة. تثبت ⁨Core⁩ أن كل توقيع صحيح؛ والإصدارات المتقدّمة تجعل مليونًا منها قابلًا للبقاء.

الشكل أدناه هو وحدة التوقيع الخاصّة بكل مستند داخل حلقة دفعة. توقّع كل تكرارة مستندًا واحدًا عند مستوى مُسمّى وتُنتج إمّا نتيجة موقَّعة بصورة صحيحة أو تُخفِق تلك المهمّة الواحدة — ولا تُعيد قطّ بايتات غير موقَّعة متنكّرة في صورة نتيجة.

<?php
declare(strict_types=1);
use NextPDF\Contracts\DocumentFactoryInterface;
use NextPDF\Security\Signature\CertificateInfo;
use NextPDF\Security\Signature\SignatureLevel;
use NextPDF\Exception\SignatureException;
use Psr\Log\LoggerInterface;
/**
* One signing-batch iteration: render, sign at a named level, commit or fail.
*
* The factory and the certificate source ($certInfo, the warmed signing
* material) are process-lifetime singletons; the document is disposable. A
* document that cannot be signed at the requested level fails this job loudly —
* it is never committed unsigned.
*
* @param iterable<int, callable(\NextPDF\Core\Document): \NextPDF\Core\Document> $jobs
*/
function signBatch(
DocumentFactoryInterface $factory,
CertificateInfo $certInfo,
LoggerInterface $logger,
iterable $jobs,
): void {
// The level is an explicit, ordered contract — not a flag we hope is honoured.
$level = SignatureLevel::PAdES_B_T;
foreach ($jobs as $jobId => $build) {
// Fresh, disposable unit — shares the warmed signing material only.
$doc = $factory->create();
$doc = $build($doc);
try {
// Sign over this document's own byte range, at exactly $level,
// or throw. There is no "signed lower, reported higher" path.
$doc->setSignature(certInfo: $certInfo, level: $level);
$signed = $doc->getPdfData();
} catch (SignatureException $e) {
// Fail-closed: this document does NOT continue as unsigned bytes.
// The job is failed and left for retry / dead-letter handling.
$logger->error('pdf.sign.failed', ['job_id' => $jobId, 'reason' => $e->getMessage()]);
continue;
}
// Only a correctly-signed result reaches the commit step.
commitSignedOutput($jobId, $signed);
unset($doc, $signed); // release per-document state before the next iteration
$logger->info('pdf.sign.committed', ['job_id' => $jobId, 'level' => $level->value]);
}
}

catch هو السطر الحامل للحِمل. إنه الفرق بين تشغيل يحتجز المستندات التي تعذّر توقيعها وتشغيل يُصدِرها رغم ذلك. وcontinue لا يطمس الإخفاق — إذ تُسجَّل المهمّة وتُترَك لإعادة المحاولة، فتنتهي الدفعة بقائمة معروفة كاملة بما وُقِّع وما لم يُوقَّع، لا بفجوة صامتة قطّ.

سوء الفهم الأول هو أن “التوقيع الدفعي” يعني توقيعًا واحدًا مطبَّقًا على ملفات كثيرة. وليس كذلك، وأيّ نظام يدّعي ذلك لا ينتج توقيعات ⁨PAdES⁩ صحيحة — إذ يُربَط ملخّص كل مستند ببايتاته الخاصّة (Spec: ISO 32000-2, §12.8). الدفعة تتعلّق محضًا بـكم العدد ومدى السرعة، لا بمشاركة الوحدة التشفيرية قطّ.

والثاني هو أن التزامن يعني تخفيف الصحّة من أجل السرعة — أن الموقِّع السريع لا بدّ أن يقطع زاوية لا يقطعها الموقِّع المتأنّي. وليس كذلك. لأن وحدات التوقيع لا تتشارك حالة قابلة للتغيير، فتشغيلها بالتوازي يغيّر الجدولة، لا البايتات. ويُحسَب كل توقيع متوازٍ بالدقّة نفسها التي يُحسَب بها التوقيع المفرد؛ والتوازي في التنسيق المحيط بها.

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

  • كل توقيع خاصّ بمستند ومرتبط بالمعايير؛ لا اختصار للدفعة. الحجم يغيّر الجدولة، لا الوحدة التشفيرية. يوقّع ⁨NextPDF⁩ كل مستند على نطاق بايتاته الخاصّ.
  • تؤدّي ⁨Core⁩ توقيع ⁨CMS⁩ برمجيًّا و⁨PAdES B-B⁩ (⁨B-T⁩ عبر عميل طابع زمني). أما محرّك العرض والتوقيع الدائم المتزامن مرّة واحدة بالضبط فهو وحدة ⁨Stream⁩ في الإصدارات المتقدّمة؛ وحفظ المفاتيح المدعوم بـ⁨HSM⁩/⁨KMS⁩ حدٌّ من حدود الإصدارات المتقدّمة. لا تدّعي هذه الصفحة ذلك التنسيق ضمن ⁨Core⁩.
  • الفشل المغلق سلوك المحرّك، لا ضمان بشأن توصيلك. يرفض ⁨NextPDF⁩ إصدار ملف غير موقَّع لكن يُظَنّ موقَّعًا ويُبرز مسار التوقيع المدعوم. أما خطّ معالجة يلتقط الخطأ الناتج ويلتزم رغم ذلك فقد اختار هزيمة الضمان — وهو التأطير الذي يوجد catch/continue في المثال لمنعه.
  • يُفرَض مستوى ⁨PAdES⁩ لكل مستند، لا يُصدَّق للتشغيل. ينتج المحرّك مستوى خط الأساس المطلوب أو يُخفِق؛ وذلك فرض بنيوي، لا حُكم مطابقة من طرف ثالث للملفات المنتَجة. أما تدرّج المستوى نفسه فمُغطّى في أنماط ⁨PAdES⁩ الأساسية.
  • الطابور، وحفظ المفتاح، وسلطة الطوابع الزمنية، ومخزن الكائنات ملكك. يوفّر ⁨NextPDF⁩ صحّة التوقيع الخاصّة بكل مستند، وفي الإصدارات المتقدّمة، أوّليات التنسيق الدائم. وهو لا يشغّل بنيتك التحتية ولا يضمن سلطة طوابعك الزمنية.
High-volume and concurrent signing — edition availability
EditionAvailability
Core

توقيع CMS برمجي خاصّ بكل مستند، وPAdES B-B (B-T مع عميل طابع زمني)، مُوقَّع منفردًا على نطاق بايتات كل مستند الخاصّ، يفشل مغلقًا ضدّ المخرَج غير الموقَّع بصمت. لا يحتاج التوقيع العادي الخاصّ بكل مستند إلى أيّ فئة تجارية.

Pro

يضيف وحدة Stream: محرّك عرض خالٍ من الأثر الجانبي إضافة إلى مُلتزِم دائم، ومخازن نقاط تفتيش وتعطيل متكرّر ورسائل مهملة — تشغيلات دفعة متزامنة، آمنة عند الانهيار، مرّة واحدة بالضبط، تستأنف بدلًا من إعادة البدء.

Enterprise

يضيف حفظ المفاتيح المدعوم بالعتاد (HSM عبر PKCS#11، أو KMS سحابي) بحيث لا يغادر المفتاح الخاصّ الجهاز قطّ، ومستويات PAdES طويلة الأمد (B-LT، B-LTA) التي تُبقي أرشيفًا عالي الحجم قابلًا للتحقّق لعقود.

  • التوقيع الدفعي — توقيع مستندات كثيرة وفق جدولة. مفهوم جدولة؛ ومع ذلك يُوقَّع كل مستند منفردًا على بايتاته الخاصّة.
  • الفشل المغلق — عند إخفاق كان سيُنتج مخرجًا غير موقَّع أو خاطئًا، يحتجز خطّ المعالجة المستند ويُبلِّغ، بدلًا من تمريره بوصفه بايتات عادية.
  • الالتزام مرّة واحدة بالضبط — خاصّة خطّ معالجة دائم حيث يُنشَر المخرَج الموقَّع بصورة صحيحة مرّة واحدة ولا يُعاد إصداره عند استئناف تشغيل منهار.
  • نقطة التفتيش — سجلّ دائم خاصّ بكل مستند لما الذي التُزِم، كي يستطيع تشغيل أن يواصل من حيث توقّف بدلًا من إعادة توقيع كل شيء.
  • CMS SignedData — الحاوية التشفيرية للتوقيعات على المحتوى (يمكنها حمل عدّة موقِّعين)؛ ينتج هذا الخطّ توقيع ⁨PDF⁩ لموقِّع واحد لكل مستند، الوحدة الخاصّة بكل مستند التي تنتجها الدفعة.
  • ⁨PAdES⁩ — التوقيعات الإلكترونية المتقدّمة لـ⁨PDF⁩ (⁨PDF⁩ Advanced Electronic Signatures)، عائلة نمط ⁨ETSI EN 319 142⁩ لتوقيع ⁨PDF⁩؛ وتمتدّ مستويات خط أساسها من ⁨B-B⁩ إلى ⁨B-LTA⁩.