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

كيف تتموضع التوقيعات داخل ملف PDF

Spec: ETSI EN 319 142-1 Spec: RFC 5652 Evidence: Standard-backed

لا يُغلّف توقيع ⁨PDF⁩ الملف. بل يُضمَّن داخله: قاموس يُسمّي التوقيع، وملخّص (⁨digest⁩) يُحسَب على نطاق مُعلَن من البايتات يتخطّى عمدًا قيمة التوقيع نفسها. تشرح هذه الصفحة تلك الآلية، وبالقدر نفسه من الأهمية، ما الذي لا تَعِد به.

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

  • يوجد توقيع ⁨PDF⁩ في قاموس توقيع وحقل توقيع داخل المستند، وليس بوصفه غلافًا خارجيًا.
  • تُعلَن البايتات الموقَّعة بواسطة مصفوفة ByteRange: مقطعان من نوع (offset, length) يغطّيان معًا الملف بأكمله باستثناء قيمة التوقيع السداسية العشرية المحفوظة في مدخل Contents.
  • ملخّص هذين المقطعين المتسلسلين هو ما يحميه التوقيع التشفيري فعلًا.
  • أيّ شيء يُلحَق لاحقًا في مراجعة جديدة يقع خارج نطاق البايتات الأصلي. يبقى التوقيع الأصلي صحيحًا؛ فهو لم يدّعِ شيئًا قط بشأن البايتات الجديدة.
  • يختلف توقيع الموافقة عن توقيع الاعتماد في النطاق: الاعتماد (DocMDP) يقيّد ما يُسمح به من تغييرات لاحقة؛ أما الموافقة فلا تفعل ذلك.

يبني ⁨NextPDF⁩ التوقيع وفق نموذج التنسيق، بترتيب ثابت، بحيث يكون نطاق البايتات دقيقًا لا تقريبيًا.

عندما يكتب المحرّك توقيعًا، يحجز أولًا فتحة ثابتة الحجم لقيمة Contents ويكتب عنصرًا نائبًا ByteRange بعرض ثابت. وهو ينتظر حتى يُكتَب المستند بالكامل، بما في ذلك جدول الإسناد الترافقي وعلامة نهاية الملف. عندئذٍ فقط يحسب الإزاحتين الحقيقيتين، ويعيد كتابتهما في العنصر النائب دون إزاحة أيّ بايت، ويحسب تجزئة المقطعين، ثم يضع كائن ⁨CMS⁩ الناتج في الفتحة المحجوزة. العنصر النائب مُبطَّن بالأصفار إلى طول ثابت، تحديدًا كي لا يؤدي ملء الأرقام الحقيقية إلى تحريك البايتات التي تُجزَّأ. هذا هو الترتيب الوحيد الذي ينتج توقيعًا متّسقًا مع نفسه. يتعامل المحرّك مع أيّ إخفاق في هذا التسلسل بوصفه خطأً قاطعًا لا تراجعًا صامتًا.

في نمط ⁨PDF 2.0⁩، يكون كائن التوقيع نفسه بنية ⁨CMS⁩ منفصلة من نوع SignedData. يبيّن قاموس PDF أين وكيف؛ أما كائن CMS فيحمل مَن والإثبات التشفيري.

  1. Step 1 of 4: ISO 32000-2 §12.8.1 — ByteRange digest & signature dictionary
  2. Step 2 of 4: ISO 32000-2 §12.8.3.3 — ETSI.CAdES.detached SubFilter
  3. Step 3 of 4: ETSI EN 319 142-1 PAdES baseline profile
  4. Step 4 of 4: RFC 5652 CMS SignedData in Contents
أين يُعرَّف توقيع PDF، من تنسيق الحاوية إلى الكائن التشفيري: يحدّد ISO 32000-2 القاموس وآلية نطاق البايتات، ويصوغ ETSI EN 319 142-1 نمطه الخاص بـPAdES، ويُعرّف RFC 5652 كائن CMS SignedData الموضوع في Contents.

Evidence: Standard-backed تُعرَّف هذه الآلية في Spec: ISO 32000-2, §12.8.1 . يُحسَب ملخّص نطاق البايتات على نطاق من البايتات يُشير إليه مدخل ByteRange. ينبغي أن يشمل ذلك النطاق الملف بأكمله متضمّنًا قاموس التوقيع لكن مستثنيًا قيمة التوقيع — مدخل Contents. ByteRange هو مصفوفة من أزواج من الأعداد الصحيحة — الإزاحة الابتدائية والطول. تُستخدَم النطاقات غير المتجاورة تحديدًا كي يتمكّن الملخّص من إغفال قيمة التوقيع نفسها.

في نمط PDF 2.0، يحدّد Spec: ISO 32000-2, §12.8.3.3 أنه عندما يكون SubFilter هو ETSI.CAdES.detached، تكون قيمة Contents كائن CMS SignedData مُرمَّزًا بترميز DER — وهي البنية ذاتها Spec: RFC 5652 يُعرّفها — ونمط PAdES لذلك الكائن هو ما يصفه Spec: ETSI EN 319 142-1 .

النطاق ليس موحَّدًا بين كل التوقيعات. Spec: ISO 32000-2, §12.7.4.5 يُعرّف صلاحية MDP: القيمة 0 تجعل التوقيع توقيع موافقة، بينما القيم 13 تجعله توقيع اعتماد يقيّد أيّ التعديلات اللاحقة تُبقي المستند مطابقًا. آلية نطاق البايتات نفسها؛ لكن الوعد بشأن المستقبل مختلف.

يُطبّق محرّك NextPDF هذا بالضبط: عنصر نائب ByteRange ثابت العرض، والملخّص المتسلسل ذو المقطعين، وكائن CMS منفصل في فتحة Contents محجوزة، ولا يُنجَز ذلك إلا بعد اكتمال الملف.

نادرًا ما تبني ByteRange يدويًا. الغاية من المثال هي إظهار شكل النتيجة كي تتعرّف عليها بسهولة عند فحص ملف موقَّع.

<?php
declare(strict_types=1);
use NextPDF\Security\Signature\ByteRangeCalculator;
// Offsets the engine knows only after the whole PDF is written:
// $contentsStart — byte just before the '<' of the hex signature
// $contentsEnd — byte just after the '>' that closes it
// $fileLength — total file size in bytes
$range = ByteRangeCalculator::calculate(
contentsStart: $contentsStart,
contentsEnd: $contentsEnd,
fileLength: $fileLength,
);
// $range === [0, $contentsStart, $contentsEnd, $fileLength - $contentsEnd]
// Segment 1: file start → just before the signature value
// Segment 2: just after the signature value → end of file
// The signature value itself is the gap. It is never hashed.
$signedMessage = ByteRangeCalculator::extractSignedData($pdfBytes, $range);
// $signedMessage is segment 1 concatenated with segment 2 — exactly the
// bytes the cryptographic digest is computed over.

الفجوة بين المقطعين هي قيمة التوقيع. لا يمكن أن تكون هذه القيمة جزءًا من ملخّصها نفسه، ولهذا يكون النطاق قطعتين لا قطعة واحدة.

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

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

ما يوفّره المحرّك، حسب الفئة، هو القدرة على بناء البنية:

PAdES signature structure (byte range, signature dictionary, detached CMS) — edition availability
Edition Availability
Core

PAdES B-B: قاموس التوقيع، وByteRange ثابت العرض، وكائن CMS SignedData المنفصل الموصوف في هذه الصفحة.

Pro

يضيف PAdES B-T — طابعًا زمنيًا موثوقًا على قيمة التوقيع — إلى البنية نفسها.

Enterprise

يضيف الأنماط طويلة الأمد (B-LT، B-LTA): مادة تحقّق مُضمَّنة وطوابع زمنية للمستند مُركَّبة على أساس نطاق البايتات نفسه.

  • قاموس التوقيع — قاموس PDF الذي يُسمّي معالج التوقيع، وSubFilter، وByteRange، وقيمة Contents.
  • ByteRange — مصفوفة من أزواج الأعداد الصحيحة (offset, length) تُعلن البايتات التي يغطّيها ملخّص التوقيع بالضبط.
  • Contents — المدخل السداسي العشري الذي يحفظ قيمة التوقيع (في PDF 2.0، كائن CMS SignedData منفصل)؛ وهو مُستثنى من ملخّصه نفسه.
  • CMS SignedData — بنية صياغة الرسائل التشفيرية (Cryptographic Message Syntax) (RFC 5652) تحمل شهادة الموقِّع وبايتات التوقيع.
  • PAdES — التوقيعات الإلكترونية المتقدّمة لـPDF (PDF Advanced Electronic Signatures): نمط ETSI لتوقيعات CMS الخاصة بـPDF، المُعرَّف في سلسلة ETSI EN 319 142.
  • توقيع الموافقة — توقيع بصلاحية MDP قيمتها 0؛ وهو يؤكّد المحتوى دون تقييد التغييرات اللاحقة.
  • توقيع الاعتماد — توقيع بصلاحية DocMDP (MDP 13) يُقيّد أيّ التعديلات اللاحقة تُبقي المستند مطابقًا.