مقال تقني

DocMDP وFieldMDP: تدقيق مراجعات PDF في Delphi

ملف PDF موقَّع تغيّر بعد التوقيع ليس معطوبًا تلقائيًا. تسمح ISO 32000-1 بالتحديثات التزايدية فوق توقيع، وبعضها فقط يخالف السياسة التي وضعها الموقِّع. يجيب HotPDF Component لـ Delphi وC++Builder على هذا السؤال بـ AnalyzeLoadedSignatureRevisions، التي تصنّف كل مراجعة لاحقة للتوقيع وتقيّمها وفق DocMDP وFieldMDP. السيناريو مألوف لكل من يشحن برمجيات عقود: يوقّع عميلك اتفاقية شراء، ويُرسلها، ويستعيدها مع صفحة ملحق مرفقة. يعرض القارئ شريطًا أصفر يقول إن التوقيع سليم لكن المستند تغيّر منذ توقيعه، ولا أحد في الغرفة يستطيع القول ما إذا كان هذا سير عمل توقيع مضاد طبيعيًا أو شخصًا يُحرّر عقدًا موقَّعًا بهدوء

ما الذي يُعدّ تغييرًا شرعيًا بعد التوقيع؟

التغيير شرعي عندما تقع فئته الدلالية ضمن الإذن الذي أعلنه التوقيع المُصدِّق. تُعرّف الفقرة §12.8.2.2 من ISO 32000-1 تحويل DocMDP بقيمة /P تساوي 1 أو 2 أو 3: 1 لا تسمح بأي تغييرات إطلاقًا، 2 تسمح بتعبئة النماذج والتوقيع، 3 تسمح بتعبئة النماذج والتوقيع والتعليقات التوضيحية. يعرض HotPDF هذه كقيم THPDFDocMDPPermission هي dmpNoChanges وdmpFormFillAndSign وdmpFormFillSignAndAnnotate، مع dmpNone محجوزة لنتائج الفحص التي لا تحمل أي تحويل DocMDP إطلاقًا

الفئات مرتَّبة، وهذا الترتيب هو محرك الفحص بأكمله. تُشغّل THPDFRevisionModificationLevel القيم rmlNone وrmlLongTermValidation وrmlFormFillAndSign وrmlAnnotations وrmlOther، مرتَّبة عمدًا بحيث لا يكون الترتيب الأكبر أقل تقييدًا أبدًا. يختزل المستند بأكمله إلى الحد الأقصى للمستوى المُلاحَظ عبر كل مراجعة بعد التوقيع، وتصبح مقارنة DocMDP اختبار عدد صحيح واحدًا. تفصيلة واحدة مهمة مبكرًا: عند dmpNoChanges لا يزال التحليل يقبل rmlLongTermValidation. إضافة مادة تحقق DSS وVRI أو طابع زمني للمستند إلى ملف مُصدَّق هي صيانة للتوقيع، لا تعديل للمستند، ومعاملتها كمخالفة ستُعطّل كل سير عمل أرشفة طويلة الأمد موجود

كيف يُعيد HotPDF بناء سلسلة المراجعات؟

بنيويًا، لا استدلاليًا. وفق الفقرة §7.5.6 من ISO 32000-1، التحديث التزايدي يُلحق قسم مراجع متقاطعة جديدًا تشير /Prev فيه إلى القسم السابق، لذا يقرأ HotPDF startxref من نهاية الملف، ويحلل القسم هناك، ويتبع /Prev إلى الخلف ويكرر، مُعيدًا الأقسام من الأقدم إلى الأحدث. حدّان أمانيان يقعان في تلك الحلقة وكلاهما يستحق المعرفة عند تشخيص ملف يفشل: /Prev يشير إلى إزاحة زُورت بالفعل ينهي الجولة بتشخيص دورة صريح بدلًا من الدوران، وسلسلة أطول من ألف مراجعة تُرفض تمامًا. كلاهما يظهر في Analysis.Issue مع إعادة الدالة False، ولا ينبغي التستر على أي منهما، لأن /Prev دوريًا ملف مشوّه أو عدائي لا ملف غير اعتيادي فحسب

أربعة أشكال تاريخية تظهر في مستندات حقيقية وكلها الأربعة مُعالجة: جداول xref التقليدية المحلَّلة سطرًا سطرًا، ودفقات المراجع المتقاطعة المفكوكة الضغط والمفكوكة الترميز عبر حقلي /W و/Index، والملفات هجينة المرجع التي يحمل تذييلها التقليدي مفتاح /XRefStm يُحلَّل ويُدمج في نفس المراجعة (حالة منتج Office، المشروحة في مقال دفقات المراجع المتقاطعة الهجينة)، والكائنات التي تعيش داخل حاوية ObjStm، وهي مهمة لأن التحديث الحديث عادةً ما يضع القاموس المتغيّر في دفق مضغوط بدلًا من كتابته مباشرةً، كما هو موصوف في مقال دفقات الكائنات والتحديثات التزايدية. التوقيع يُثبّت نقطة الانقسام: /ByteRange[2] + /ByteRange[3] يصبح SignedRevisionLength، وكل قسم عند تلك الإزاحة أو بعدها يكون لاحقًا للتوقيع. أما ما إذا كان نطاق البايتات لا يزال يُجزَّأ بشكل صحيح فسؤال منفصل، تجيب عنه VerifyLoadedSignature والمشروح في مقال التحقق من توقيعات PDF الرقمية

كيف يُصنَّف كل كائن مُتغيّر؟

التصنيف يُنفَّذ لكل كائن، ثم ينتشر عبر المراجع. لكل رقم كائن يمسّه قسم لاحق للتوقيع، يقرأ HotPDF المتن الجديد والمتن كما كان في اللقطة الموقَّعة؛ متن متطابق هو rmlNone، لأن المنتجين يُعيدون كتابة الكائنات فعلًا دون تغييرها. أدوات التمييز ضيقة عمدًا. كائن /Type /DocTimeStamp، أو واحد يكون /SubFilter فيه ETSI.RFC3161، هو rmlLongTermValidation، وكذلك أي شيء يمكن الوصول إليه من شجرة /DSS في الفهرس؛ قاموس /Type /Sig هو rmlFormFillAndSign. للحاويات، الاختبار هو أي المفاتيح تحرّكت، لا ما هو الكائن: قد يكتسب الفهرس أو يُعدِّل فقط /DSS أو /Extensions أو /AcroForm؛ وقاموس AcroForm فقط /Fields أو /SigFlags أو /NeedAppearances أو /DR أو /DA أو /Q؛ وصفحة فقط /Annots؛ وحقل أو ودجت فقط /V أو /AP أو /AS أو /M. أي شيء خارج تلك المجموعات ينخفض إلى rmlOther، وهذا بالضبط كيف تُضبَط صفحة الملحق المُلحقة: إضافة صفحة تُعيد ترتيب شجرة الصفحات بطرق لا تغطيها أي قائمة سماح، ولا أي قدر من تعبئة نماذج مشروعة يشبهها

ثم تنتشر المستويات، مع وراثة كل حاوية للحد الأقصى لمستوى الأبناء المتغيّرين الذين تشير إليهم، مُكررة حتى يستقر التعيين. هذا ما يجعل دفقات المظهر تعمل. حقل نصي مُعبَّأ يُعيد كتابة /V ويشير إلى دفق /AP جديد، وذلك الدفق بمفرده كتلة مجهولة من عوامل تشغيل المحتوى بلا نوع يُميَّز؛ ولأن الحقل الذي يملكه هو rmlFormFillAndSign، يرث الدفق نفس المستوى بدلًا من السقوط إلى rmlOther. نفس الانتشار يحمل سياق DSS إلى دفقات الشهادات والإلغاء التي لولا ذلك لكانت غير قابلة للتصنيف

لماذا يُعدّ كائن غير قابل للقراءة مخالفة؟

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

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

قراءة الحكم في Delphi

الاستدعاء قصير. حمّل المستند، اختر فهرس توقيع، اقرأ السجل؛ التحميل الزائد بلا وسيطات يُعيد فتح الملف الذي حُمِّل منه المستند، وتحميل TStream الزائد يأخذ بايتات موفَّرة من المستدعي ويستعيد موضع الدفق قبل الإرجاع. PolicyCompliant هي القيمة المنطقية الوحيدة التي يريدها معظم المستدعين، وتجمع ثلاثة قرارات مستقلة: الصحة البنيوية لقواميس الأذونات، وDocMDPCompliant، وFieldMDPCompliant. أبقِ المكوّنات مرئية في واجهتك بدلًا من طيّها، ولاحظ أن مستندًا بلا تحويل DocMDP يترك DocMDPCompliant عند True، لأن توقيع موافقة عادي لا يُعلن سياسة يمكن مخالفتها، وحينها يكون ModificationLevel الإجمالي وصفيًا لا حكمًا

var
  Pdf: THotPDF;
  Analysis: THPDFSignatureRevisionAnalysis;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile('contract-countersigned.pdf') > 0 then
    begin
      if Pdf.AnalyzeLoadedSignatureRevisions(0, Analysis) then
      begin
        if Analysis.PolicyCompliant then
          Writeln('Post-signature changes stay inside the signing policy')
        else
          Writeln('Policy violation: ', string(Analysis.Issue));
      end
      else
        Writeln('Analysis could not run: ', string(Analysis.Issue));
    end;
  finally
    Pdf.Free;
  end;
end;

للفرز عادةً ما تريد التفصيل لكل مراجعة بدلًا من الملخص، لأنه يُخبرك متى في تاريخ المستند حدث الخلل. كل مدخل في Analysis.Revisions يحمل فهرسه في السلسلة، وإزاحة المراجع المتقاطعة التي كُتب عندها، ومستوى تعديله الخاص، وأرقام الكائنات المتضمّنة

const
  LevelNames: array[THPDFRevisionModificationLevel] of string =
    ('none', 'long-term validation', 'form fill and sign',
     'annotations', 'other');
var
  I: Integer;
begin
  Writeln(Format('%d revisions in chain, signature sits at index %d',
    [Analysis.TotalRevisionCount, Analysis.SignedRevisionIndex]));
  for I := 0 to High(Analysis.Revisions) do
    Writeln(Format('  rev %d at offset %d: %s (%d changed, %d freed) %s',
      [Analysis.Revisions[I].RevisionIndex,
       Analysis.Revisions[I].XRefOffset,
       LevelNames[Analysis.Revisions[I].ModificationLevel],
       Length(Analysis.Revisions[I].ChangedObjectNumbers),
       Length(Analysis.Revisions[I].FreedObjectNumbers),
       string(Analysis.Revisions[I].Issue)]));
end;

يُقيَّم FieldMDP بشكل منفصل، وهذا مقصود

يمكن لمستند أن يستوفي DocMDP ويبقى غير شرعي، ولهذا فإن FieldMDPCompliant قيمة منطقية منفصلة بدلًا من طيّها في مقارنة المستوى. تُعرّف الفقرة §12.8.2.4 من ISO 32000-1 تحويل FieldMDP، والفقرة §12.7.5.5 مدخل /SigFieldLock ذا الصلة، لتجميد حقول نماذج مُسمّاة عند لحظة التوقيع حتى حين لا يزال المستند ككل يسمح بتعبئة النماذج. تعبئة حقل عمل من المستوى 2؛ تعبئة حقل قفله الموقِّع مخالفة بصرف النظر عن المستوى. يقرأ HotPDF النطاق إلى THPDFFieldLockAction كـ flaAll أو flaInclude أو flaExclude، مع flaNone للنتائج التي لا تحمل سياسة قفل، والأسماء إلى Permissions.FieldNames: flaAll تقفل كل شيء، وflaInclude تقفل الأسماء المُدرَجة، وflaExclude تقفل كل شيء ما عداها. تفصيلة واحدة مهمة عند قراءة النتائج، وهي أن الحقول الموجودة بالفعل في اللقطة الموقَّعة فقط تُبلَّغ في ChangedFieldNames، لأن حقلًا أُنشئ بالكامل بعد التوقيع ليس له حالة موقَّعة يناقضها ويُضبَط عبر مسار DocMDP بدلًا من ذلك

var
  Source: TFileStream;
  Analysis: THPDFSignatureRevisionAnalysis;
  I: Integer;
begin
  Source := TFileStream.Create('contract.pdf', fmOpenRead or fmShareDenyWrite);
  try
    if Pdf.AnalyzeLoadedSignatureRevisions(0, Source, Analysis) then
      if Analysis.Permissions.HasFieldMDP and (not Analysis.FieldMDPCompliant) then
        for I := 0 to High(Analysis.ChangedFieldNames) do
          Writeln('modified after locking: ',
            string(Analysis.ChangedFieldNames[I]));
  finally
    Source.Free;  // stream position was restored before the call returned
  end;
end;

ما لا يخبرك به هذا التحليل

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

كل ما وُصف هنا يعمل أصليًا في Delphi وC++Builder دون خدمة توقيع خارجية في الحلقة، وهذا ما يجعله عمليًا للتشغيل على كل مستند وارد بدلًا من المستندات التي اشتبه بها أحد بالفعل فقط. واجهة التوقيع والمراجعات الكاملة، بما في ذلك طرق الأذونات والتحقق التي تقترن بها، جزء من HotPDF Component لـ Delphi وC++Builder