مقاله فنی

DocMDP و FieldMDP: ممیزی نسخه‌های PDF در Delphi

یک PDF امضاشده که پس از امضا تغییر کرده خودبه‌خود خراب نیست. ISO 32000-1 به‌روزرسانی‌های افزایشی روی یک امضا را مجاز می‌داند، و فقط برخی از آن‌ها خط‌مشی‌ای را که امضاکننده تنظیم کرده می‌شکنند. HotPDF Component برای Delphi و C++Builder به آن سؤال با AnalyzeLoadedSignatureRevisions پاسخ می‌دهد، که هر revision پس از امضا را طبقه‌بندی می‌کند و آن را در برابر DocMDP و FieldMDP نمره‌دهی می‌کند. این سناریو برای هرکسی که نرم‌افزار قرارداد عرضه می‌کند آشناست: مشتری شما یک توافق‌نامه خرید را امضا می‌کند، آن را ارسال می‌کند، و آن را با یک صفحه ضمیمه چسبیده به آن پس می‌گیرد. reader یک نوار زرد نشان می‌دهد که می‌گوید امضا سالم است اما سند از زمان امضا تغییر کرده، و هیچ‌کس در اتاق نمی‌تواند بگوید آیا این یک workflow عادی امضای دوم است یا کسی در سکوت یک قرارداد امضاشده را ویرایش می‌کند

چه چیزی پس از امضا یک تغییر قانونی محسوب می‌شود؟

یک تغییر زمانی قانونی است که دسته معنایی آن داخل مجوزی بیفتد که امضای تأییدی (certifying) اعلام کرده. ISO 32000-1 §12.8.2.2 تبدیل DocMDP را با مقدار /P برابر ۱، ۲ یا ۳ تعریف می‌کند: ۱ هیچ تغییری را مجاز نمی‌کند، ۲ پرکردن فرم و امضا را مجاز می‌کند، ۳ پرکردن فرم، امضا و annotation را مجاز می‌کند. HotPDF این‌ها را به‌عنوان مقادیر THPDFDocMDPPermission با نام‌های dmpNoChanges، dmpFormFillAndSign و dmpFormFillSignAndAnnotate ارائه می‌دهد، با dmpNone که برای نتایج بازرسی‌ای که هیچ تبدیل DocMDP حمل نمی‌کنند رزرو شده

دسته‌ها ترتیبی هستند، و آن ترتیب موتور کل بررسی است. THPDFRevisionModificationLevel rmlNone، rmlLongTermValidation، rmlFormFillAndSign، rmlAnnotations، rmlOther را اجرا می‌کند، عمداً طوری چیده شده که یک ordinal بزرگ‌تر هرگز کم‌محدودتر نباشد. کل یک سند به بیشینه سطح مشاهده‌شده در سراسر هر revision پس از امضا کاهش می‌یابد، و مقایسه DocMDP یک آزمون عدد صحیح واحد می‌شود. یک ظرافت زود اهمیت پیدا می‌کند: در dmpNoChanges، تحلیل همچنان rmlLongTermValidation را می‌پذیرد. اضافه کردن مواد اعتبارسنجی DSS و VRI یا یک timestamp سند به یک فایل تأییدشده نگهداری امضاست، نه تغییر سند، و رفتار با آن به‌عنوان تخلف هر workflow آرشیو بلندمدت موجود را می‌شکند

HotPDF چگونه زنجیره revision را بازسازی می‌کند؟

ساختاری، نه اکتشافی. طبق ISO 32000-1 §7.5.6 یک به‌روزرسانی افزایشی یک بخش cross-reference جدید اضافه می‌کند که /Prev آن به بخش قبلی اشاره می‌کند، بنابراین HotPDF startxref را از انتها می‌خواند، بخش آنجا را parse می‌کند، /Prev را به عقب دنبال می‌کند و تکرار می‌کند، بخش‌ها را از قدیم‌ترین به جدیدترین برمی‌گرداند. دو محدودیت ایمنی در آن حلقه نشسته‌اند و هر دو ارزش دانستن دارند وقتی یک فایل که شکست می‌خورد را triage می‌کنید: یک /Prev که به یک offset از پیش بازدید‌شده اشاره می‌کند پیمایش را با یک تشخیص چرخه صریح خاتمه می‌دهد به‌جای چرخیدن، و یک زنجیره طولانی‌تر از هزار revision کاملاً رد می‌شود. هر دو در Analysis.Issue با برگشت False تابع ظاهر می‌شوند، و هیچ‌کدام نباید روپوش شوند، چون یک /Prev چرخه‌ای یک فایل ناقص یا خصمانه است نه یک فایل غیرمعمول

چهار شکل تاریخی در اسناد واقعی ظاهر می‌شوند و هر چهار مدیریت می‌شوند: جداول xref سنتی که خط‌به‌خط parse شده‌اند، cross-reference streamهایی که از طریق فیلدهای /W و /Index خود decompress و decode شده‌اند، فایل‌های hybrid-reference که trailer سنتی‌شان کلید /XRefStm حمل می‌کند که parse می‌شود و در همان revision ادغام می‌شود (حالت producer آفیس، پوشش‌داده‌شده در مقاله hybrid cross-reference streamها)، و اشیائی که داخل یک container ObjStm زندگی می‌کنند، که اهمیت دارند چون یک به‌روزرسانی مدرن معمولاً دیکشنری تغییریافته را در یک stream فشرده قرار می‌دهد به‌جای نوشتن مستقیم آن، همان‌طور که در مقاله object streamها و به‌روزرسانی‌های افزایشی شرح داده شده. امضا نقطه تقسیم را لنگر می‌کند: /ByteRange[2] + /ByteRange[3] به SignedRevisionLength تبدیل می‌شود، و هر بخش در آن offset یا فراتر از آن پس از امضاست. اینکه آیا بازه بایت هنوز درست هش می‌شود سؤال دیگری است، که با VerifyLoadedSignature پاسخ داده می‌شود و در مقاله بررسی امضاهای دیجیتال PDF پوشش داده شده

هر شیء تغییریافته چگونه طبقه‌بندی می‌شود

طبقه‌بندی به ازای هر شیء اجرا می‌شود، سپس در امتداد ارجاعات منتشر می‌شود. برای هر شماره شیءای که یک بخش پس از امضا لمس می‌کند، HotPDF بدنه جدید و بدنه‌ای که در snapshot امضاشده بوده را می‌خواند؛ یک بدنه یکسان rmlNone است، چون producerها بدون تغییر اشیاء را بازنویسی می‌کنند. تشخیص‌دهنده‌ها عمداً باریک‌اند. یک شیء /Type /DocTimeStamp، یا یکی که /SubFilter آن ETSI.RFC3161 است، rmlLongTermValidation است، همان‌طور که هرچیز قابل‌دسترسی از درخت /DSS کاتالوگ نیز چنین است؛ یک دیکشنری /Type /Sig rmlFormFillAndSign است. برای containerها آزمون این است که کدام کلید حرکت کرده، نه اینکه شیء چیست: کاتالوگ فقط ممکن است /DSS، /Extensions یا /AcroForm را کسب یا تغییر دهد؛ دیکشنری AcroForm فقط /Fields، /SigFlags، /NeedAppearances، /DR، /DA یا /Q؛ یک صفحه فقط /Annots؛ یک field یا widget فقط /V، /AP، /AS یا /M. هرچیز خارج از آن مجموعه‌ها به rmlOther سقوط می‌کند، که دقیقاً همان‌طور است که صفحه ضمیمه اضافه‌شده گرفته می‌شود: اضافه کردن یک صفحه درخت صفحه را به روش‌هایی مرتب می‌کند که هیچ whitelist پوشش نمی‌دهد، و هیچ مقدار پرکردن فرم قانونی شبیه آن نیست

سپس سطوح منتشر می‌شوند، با هر container که بیشینه سطح فرزندان تغییریافته‌ای که به آن‌ها اشاره می‌کند را به ارث می‌برد، تکرارشده تا واگذاری تثبیت شود. این همان چیزی است که appearance streamها را کاری می‌کند. یک فیلد متنی پرشده /V را بازمی‌نویسد و به یک stream /AP تازه اشاره می‌کند، و آن stream به‌تنهایی یک blob ناشناس از operatorهای محتوا است بدون هیچ نوعی برای شناسایی؛ چون فیلدی که مالک آن است rmlFormFillAndSign است، stream همان سطح را به ارث می‌برد به‌جای سقوط به rmlOther. همان انتشار زمینه DSS را روی streamهای گواهی و ابطال حمل می‌کند که در غیر این صورت غیرقابل‌طبقه‌بندی بودند

چرا یک شیء غیرقابل‌خواندن یک تخلف محسوب می‌شود؟

چون جایگزین یک اعتبارسنج است که با نوشتن چیزی که نمی‌فهمد شکست خورده. سه موقعیت بدون هیچ فرصت درخواست تجدیدنظر در HotPDF به rmlOther ختم می‌شوند: یک شیء که بدنه‌اش نتوانست از revision خوانده شود، یک شیء که revision آن را به‌عنوان free علامت‌گذاری می‌کند، و یک شیء که با هیچ‌کدام از تشخیص‌دهنده‌های بالا مطابقت ندارد. هر کدام یک تشخیص خاص در فیلد Issue revision ثبت می‌کنند، بنابراین یک اپراتور می‌تواند ببیند کدام شماره شیء آن حکم را تولید کرده

free کردن تیزترین این سه است. یک revision پس از امضا که یک شیء از پیش تعریف‌شده را به‌عنوان free علامت‌گذاری می‌کند محتوا را از یک سند امضاشده حذف کرده، و هیچ سطح مجوزی زیر §12.8.2.2 آن را مجاز نمی‌کند؛ شماره‌های شیء در FreedObjectNumbers فرود می‌آیند و revision به rmlOther ارتقا می‌یابد. اشیاء غیرقابل‌خواندن به همان منطق برای دلیلی متفاوت پیروی می‌کنند. یک اعتبارسنج که نمی‌تواند یک شیء را parse کند هیچ مبنایی برای بی‌ضرر خواندن آن ندارد، و پاسخ صادقانه به آن سکوت نیست. گزارش یک ساختار غیرمعمول اما بی‌ضرر به‌عنوان یک تخلف هزینه یک بازبینی انسانی دارد؛ خطای معکوس یک قرارداد امضاشده با یک ویرایش نادیده‌گرفته‌شده داخل آن عرضه می‌کند

خواندن حکم در Delphi

فراخوانی کوتاه است. سند را بارگذاری کنید، یک اندیس امضا انتخاب کنید، رکورد را بخوانید؛ overload بدون پارامتر فایلی را که سند از آن بارگذاری شده دوباره باز می‌کند، و overload TStream بایت‌های تأمین‌شده توسط فراخواننده را می‌گیرد و موقعیت stream را پیش از بازگشت بازیابی می‌کند. PolicyCompliant boolean واحدی است که اغلب فراخوانندگان می‌خواهند، که سه تصمیم مستقل را ترکیب می‌کند: اعتبار ساختاری دیکشنری‌های مجوز، DocMDPCompliant، و FieldMDPCompliant. مؤلفه‌ها را در UI خود قابل‌مشاهده نگه دارید به‌جای فروپاشی آن‌ها، و توجه کنید سندی بدون تبدیل 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;

برای triage معمولاً به تفکیک به ازای هر revision نیاز دارید به‌جای خلاصه، چون می‌گوید چه زمانی در تاریخ سند چیزها اشتباه رفته. هر entry در Analysis.Revisions اندیس خود در زنجیره، offset cross-reference که در آن نوشته شده، سطح تغییر خودش، و شماره‌های شیء درگیر را حمل می‌کند

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 یک boolean مجزاست به‌جای تا شدن در مقایسه سطح. ISO 32000-1 §12.8.2.4 تبدیل FieldMDP را تعریف می‌کند، و §12.7.5.5 entry مرتبط /SigFieldLock را، تا فیلدهای فرم نام‌گذاری‌شده را در لحظه امضا منجمد کند حتی جایی که کل سند همچنان پرکردن فرم را مجاز می‌کند. پرکردن یک فیلد یک اقدام سطح ۲ است؛ پرکردن یک فیلدی که امضاکننده قفل کرده صرف‌نظر از سطح یک تخلف است. HotPDF دامنه را در THPDFFieldLockAction به‌صورت flaAll، flaInclude یا flaExclude می‌خواند، با flaNone برای نتایجی که هیچ خط‌مشی قفل حمل نمی‌کنند، و نام‌ها را در Permissions.FieldNames: flaAll همه‌چیز را قفل می‌کند، flaInclude نام‌های فهرست‌شده را قفل می‌کند، flaExclude همه‌چیز به‌جز آن‌ها را قفل می‌کند. یک جزئیات هنگام خواندن نتایج اهمیت دارد، به این صورت که فقط فیلدهای از پیش موجود در snapshot امضاشده در 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 blob هش می‌شود، و اینکه آیا گواهی امضاکننده به چیزی که شما اعتماد دارید زنجیر می‌شود، با VerifyLoadedSignature و VerifyLoadedSignatureWithTrust پاسخ داده می‌شوند. یک فایل می‌تواند کاملاً با خط‌مشی مطابق و از نظر رمزنگاری بی‌ارزش باشد، پس این دو بررسی در هر gate پذیرش واقعی باید کنار هم باشند. همچنین قصد درون content streamها را نمی‌خواند: صفحه‌ای که content stream آن کاملاً جایگزین شده به‌عنوان یک تغییر خارج از whitelist گرفته می‌شود، اما این تحلیل به شما نمی‌گوید جایگزینی یک رقم پرداخت را عوض کرده. یک حکم rmlOther یعنی یک انسان باید نگاه کند، نه اینکه تقلبی رخ داده، و یک حکم مطابق یعنی تغییر با یک دسته مجاز جور درمی‌آید، نه اینکه تغییر خواسته شده بوده. وقتی تنها چیزی که نیاز دارید همان چیزی است که امضاکننده اعلام کرده، بدون پیمایش revision، GetLoadedSignaturePermissions دیکشنری‌های خط‌مشی را به‌تنهایی برمی‌گرداند

همه چیزی که اینجا شرح داده شد به‌صورت بومی در Delphi و C++Builder بدون هیچ سرویس امضای خارجی در حلقه اجرا می‌شود، که همان چیزی است که اجرای آن را روی هر سند ورودی به‌جای فقط آن‌هایی که کسی از قبل مشکوک بوده عملی می‌کند. API کامل امضا و revision، شامل روش‌های مجوز و اعتبارسنجی‌ای که با آن‌ها جفت می‌شود، بخشی از HotPDF Component برای Delphi و C++Builder است