مقاله فنی

workbench compliance و signing در PDF Library for Delphi برای Delphi

یک workbench که compliance validation را به digital signing زنجیر می‌کند باید چهار step را به این ترتیب هماهنگ کند، و کل راه آن‌ها را به یک set از byte گره بزند. یک preflight در PDF/A یا PDF/UA اجرا می‌کند. هر fixای که findingها demand می‌کنند را اعمال کرده و یک revision اصلاح‌شده save می‌کند. آن revision دقیقاً همان را sign می‌کند. سپس file signed را بازخوانی می‌کند و تایید می‌کند signature واقعاً آن را پوشش می‌دهد. ترتیب برای آرایش نیست. read-back را skip کنید و به write path خودتان اعتماد می‌کنید؛ بگذارید preflight روی revision اشتباه اجرا شود و گزارش compliance شما fileای را توصیف می‌کند که هرگز ship نکرده‌اید

قسمتی که بیشتر pipelineهای homegrown اشتباه می‌گیرند درز بین validation و signing است. آن‌ها را به‌عنوان دو ابزار مجزا با یک pass remediation بین آن‌ها اجرا کنید و حداقل سه revision متمایز از file به وجود می‌آید، هر کدام با byteهای خودش. preflight reportای که به یک auditor می‌دهید یکی از آن‌ها را توصیف می‌کند. signature دیگری را freeze می‌کند. هیچ‌چیز در file بیان نمی‌کند که همان revision هستند، و اغلب نیستند. PDF Library for Delphi، کتابخانهٔ PDF Developer شرکت losLab برای Delphi و C++Builder، preflight و PAdES signing را پشت یک facade class واحد می‌گذارد، تا کل sequence بتواند در یک process زندگی کند که هرگز از کدام byteهایی صحبت می‌کند غافل نشود. هر call زیر امروز در کتابخانه موجود است، و هر تله‌ای که در کنارش ذکر شده نیز همین‌طور

نمودار میزکار انطباق و امضای Delphi که در آن گام‌های preflight، اصلاح، امضای PAdES و ممیزی ByteRange هر کدام یک SHA-256 روی دقیقاً همان نسخه‌ای که لمس می‌کنند ثبت می‌کنند
هش‌های ثبت‌شده در کنار هر ذخیره‌سازی، گزارش preflight و امضای PAdES و ممیزی را به یک بازبینی یکسان گره می‌زنند

سه revision از یک document، و چگونگی باز شدن فاصله

saveها را بشمارید. اصلی از upstream می‌رسد. pass remediation آن را load می‌کند، یک compliance mode را روشن می‌کند، و یک revision اصلاح‌شده می‌نویسد. pass signing یک signature را به‌عنوان یک incremental update اضافه می‌کند، که یک write سوم است. سه save، سه byte layout، و یک preflight report هیچ معنی‌ای ندارد مگر اینکه نام ببرد کدام یک از سه را پوشش می‌دهد. یک SHA-256 از file، ضبط‌شده کنار هر preflight run و هر signature، anchor ارزانی است که به شما اجازه می‌دهد ثابت کنید revisionای که validate کرده‌اید همان revisionای است که sign کرده‌اید

یک رفتار کتابخانه آن discipline را بیشتر tighten می‌کند. fixهای compliance درخواست‌شده از طریق SetPDFAMode یا SetPDFUAMode وقتی صدا می‌زنید آن‌ها اثر نمی‌کنند. آن‌ها هنگام save اعمال می‌شوند. auto-repairهایی مانند forcing کردن annotation print flagها یا assigning کردن یک tab order در PDF/UA در output file و هیچ‌جای دیگر land می‌شوند، پس یک check اجرا‌شده در برابر documentای که تازه «fixed» کرده‌اید در memory چیزی درباره byteهای رو به signer به شما نمی‌گوید. اول save کنید، بعد file ذخیره‌شده را preflight کنید. state در-memory یک draft است؛ تنها file روی disk واقعی است

preflight از disk، و صفر به معنای دو چیز

entry point در flat preflight CheckFileCompliance(FileName, Password, ComplianceTest, Options) است. test 1 (ISO 19005) PDF/A را انتخاب می‌کند، test 2 (ISO 14289) PDF/UA را. file را از طریق streaming reader کتابخانه باز می‌کند، پس نیازی به LoadFromFile اول نیست، و یک handle string-list برمی‌گرداند که یک finding per entry حمل می‌کند:

var
  PDF: TPDFlib;
  ListID, I: Integer;
begin
  PDF := TPDFlib.Create;
  try
    ListID := PDF.CheckFileCompliance('invoice-fixed.pdf', '', 1, 0);  // 1 = PDF/A
    if ListID = 0 then
    begin
      if PDF.LastErrorCode <> 0 then
        raise Exception.Create('Preflight could not read the file')
      else
        Writeln('No PDF/A findings');
    end
    else
    begin
      for I := 0 to PDF.GetStringListCount(ListID) - 1 do
        Writeln(PDF.GetStringListItem(ListID, I));
      PDF.ReleaseStringList(ListID);
    end;
  finally
    PDF.Free;
  end;
end;

تله در return value می‌نشیند، و از نوعی است که از هر happy-path testای عبور می‌کند. صفر یعنی «no findings». صفر همچنین یعنی «file نمی‌توانست باز شود»، چون implementation هرگاه result list خالی برگردد، شامل یک read failure، ۰ برمی‌گرداند. یک workbench که ۰ را به‌عنوان چراغ سبز می‌خواند با کمال میل fileای را تایید می‌کند که یک process دیگر lock کرده. جفت کردن call با LastErrorCode، همان‌طور که بالا، آن چیزی است که دو حالت را جدا می‌کند. checker همچنین file را با یک deny-write share mode باز می‌کند، پس اگر step remediation شما هنوز یک writer handle نگه داشته، preflight به دلیلی fail می‌شود که هیچ ربطی به compliance ندارد و همه ربطش به یک stream است که فراموش کرده‌اید free کنید

نمودار تصمیم که نشان می‌دهد LastErrorCode چگونه دو معنای بازگشت صفر از CheckFileCompliance را در preflight یک PDF در Delphi تفکیک می‌کند
صفرِ برگشتی از CheckFileCompliance تا وقتی LastErrorCode فهرست یافته‌های خالی را از فایلی که کتابخانه نتوانست باز کند جدا نکند هیچ معنایی ندارد

وقتی یک انسان به‌جای یک pipeline نیاز دارد findingها را بخواند، CreatePreflightReport آن‌ها را به‌شکل یک report قابل‌خواندن render می‌کند. ComparePreflightReports دو run را diff می‌کند، که راه مرتبی است برای نشان دادن که remediation findingهای اصلی را پاک کرد بدون اینکه بی‌صدا findingهای جدید معرفی کند

sign کردن revision بررسی‌شده با یک SignProcess

یک‌بار revision ذخیره‌شده از preflight عبور کرد و hash آن روی record شد، دقیقاً همان file و نه چیز دیگر را sign کنید. SignProcess API مانند یک builder خوانده می‌شود. یک process handle باز کنید، آن را خط به خط پیکربندی کنید، commit کنید، سپس result code را بازخوانی کنید

ProcessID := PDF.NewSignProcessFromFile('invoice-fixed.pdf', '');
if ProcessID = 0 then
  raise Exception.Create('Cannot open source for signing');
PDF.SetSignProcessField(ProcessID, 'ApprovalSig');
PDF.SetSignProcessPFXFromFile(ProcessID, 'company.pfx', PfxPassword);
PDF.SetSignProcessInfo(ProcessID, 'Invoice approval', 'Berlin', 'billing@example.com');
PDF.SetSignProcessCustomSubFilter(ProcessID, 'ETSI.CAdES.detached');  // PAdES baseline
PDF.SetSignProcessDigestAlgorithm(ProcessID, 2);                      // SHA-256
PDF.SetSignProcessReserveContentsBytes(ProcessID, 8192);              // room for a later timestamp
PDF.EndSignProcessToFile(ProcessID, 'invoice-signed.pdf');
if PDF.GetSignProcessResult(ProcessID) <> 1 then
  Writeln('Sign failed, code ', PDF.GetSignProcessResult(ProcessID));
PDF.ReleaseSignProcess(ProcessID);

دو خط در آن sequence بیشتر از آنچه به‌نظر می‌رسد وزن حمل می‌کنند. SetSignProcessCustomSubFilter با ETSI.CAdES.detached یک signature PAdES را همان‌طور که در ETSI EN 319 142-1 profile شده انتخاب می‌کند به‌جای خانوادهٔ legacy adbe.pkcs7.detached، که تفاوت بین یک signatureای است که یک validator اروپایی می‌پذیرد و یکی که آن را flag می‌کند. SetSignProcessReserveContentsBytes placeholder /Contents را pad می‌کند، و اندازه‌ای که اینجا انتخاب می‌کنید یک تصمیم درباره آینده است: اگر تا‌به‌حال یک signature timestamp قرار است پیگیری شود، CMS بزرگ‌شده باید در فضایی که الان reserve می‌کنید جا شود، چون placeholder نمی‌تواند بعداً بدون re-sign کردن کل چیز رشد کند. سخاوتمندانه reserve کنید و چند کیلوبایت هدر می‌دهید. بیش‌ازحد تنگ reserve کنید و step timestamp ماه‌ها بعد با یک overflow که ربط‌دادنش به این یک خط برایتان سخت خواهد بود، fail می‌شود

GetSignProcessResult با یک code پاسخ می‌دهد، نه یک boolean، و codeها ارزش نگه‌داشتن دارند. ۱ موفقیت است. ۴ یک PDF password اشتباه است، ۷ یک certificate password اشتباه، ۹ یک PFX که هیچ private keyای حمل نمی‌کند، ۱۱ یک failure هنگام اعمال شدن signature. آن‌ها را به یک true/false collapse کنید و تنها قطعه اطلاعاتی را دور می‌ریزید که یک case پشتیبانی wrong-password را از یک key-without-private-part جدا می‌کند. integer را log کنید

read-back: audit کردن fileای که تازه تولید کرده‌اید

هیچ workbenchای نباید به pathای که fileای را که می‌خواهد certify کند نوشته اعتماد کند. class audit TPDFlibSignDoc output signed را باز می‌کند و entryهای signature dictionary را مستقیماً از disk می‌خواند:

var
  Doc: TPDFlibSignDoc;
  Names: TStringList;
  FS: TFileStream;
  I: Integer;
  SourceSize, RangeStart, GapStart, TailStart, TailLen: Int64;
begin
  // اندازه را پیش از Open بگیر: شیء audit قفل اشتراکی روی فایل دارد
  FS := TFileStream.Create('invoice-signed.pdf', fmOpenRead or fmShareDenyNone);
  SourceSize := FS.Size;
  FS.Free;
  Doc := TPDFlibSignDoc.Create;
  Names := TStringList.Create;
  try
    if not Doc.Open('invoice-signed.pdf', '', False) then Exit;
    Doc.GetSignatureFieldNames(Names);
    for I := 0 to Names.Count - 1 do
      if Doc.GetSignatureValueObjNum(Names[I]) > 0 then  // > 0 یعنی فیلد امضا شده است
      begin
        RangeStart := StrToInt64(string(Doc.GetSignatureValueByName(Names[I], 11)));
        GapStart   := StrToInt64(string(Doc.GetSignatureValueByName(Names[I], 12)));
        TailStart  := StrToInt64(string(Doc.GetSignatureValueByName(Names[I], 13)));
        TailLen    := StrToInt64(string(Doc.GetSignatureValueByName(Names[I], 14)));
        if (RangeStart = 0) and (TailStart + TailLen = SourceSize) then
          Writeln(Names[I], ': signature covers the file to EOF')
        else
          Writeln(Names[I], ': earlier revision, or unusual ByteRange layout');
      end;
    Doc.Close;
  finally
    Names.Free;
    Doc.Free;
  end;
end;

argumentهای ValueKey روی entryهای dictionary نگاشت می‌شوند. key ۰ CMS خام از /Contents را برمی‌گرداند، keyهای ۲ و ۳ nameهای /Filter و /SubFilter، و ۱۱ تا ۱۴ چهار عدد ByteRange. valueهای متنی از طریق GetSignatureTextValueByName برمی‌گردند: key ۰ signing time ادعا‌شده است، و key ۵ یک Sig معمولی را از یک DocTimeStamp جدا می‌کند، که مهم می‌شود یک‌بار یک document هر دو را حمل کند

file-size capture در بالای آن example load-bearing است، نه housekeeping. TPDFlibSignDoc.Open file را برای کل lifetimeاش تحت یک share lock محدودکننده نگه می‌دارد، پس هرآنچه به byteهای خام نیاز دارد (hash کردن range signed، re-compute کردن CMS digest) باید file را قبل از صدا زده شدن Open بخواند. demo SigningWorkbench خود کتابخانه دقیقاً به همین دلیل کل file را اول در memory می‌خواند، و یک workbench که ترتیب را نادیده می‌گیرد به‌طور متناوب fail می‌کند، روی هر machineای که اتفاقاً race را ببازد

حساب ByteRange که coverage را ثابت می‌کند

یک file single-signature سالم یک ByteRange به‌شکل [0 a b c] دارد: coverage از offset ۰ شروع می‌شود، placeholder hex /Contents بین a و b را skip می‌کند، سپس از طریق byte b+c از سر می‌گیرد. وقتی b+c با file size برابر باشد، signature همه‌چیز را تا انتهای file پوشش می‌دهد، که نتیجه‌ای است که می‌خواهید. وقتی کم بیاورد، کسی یک incremental update بعد از نوشته‌شدن signature اضافه کرده. این تحت ISO 32000-1§12.8 کاملاً legit است، چون form fillهای بعدی، یک signature دوم، و یک dictionary DSS همگی دقیقاً به این شکل می‌رسند. این همچنین دقیقاً factای است که یک audit trail باید هنگام signing ضبط کند به‌جای اینکه تحت فشار در طول یک dispute بازسازی شود

PDF Library for Delphi: کالبدشکافی ByteRange یک PDF امضاشده با نمایش شکاف جای‌نگهدار Contents به همراه حالت پوشش کامل و حالت به‌روزرسانی افزایشی الحاقی
یک ByteRange از 0 a b c تنها زمانی کل فایل را پوشش می‌دهد که b + c به انتهای فایل برسد، بنابراین ممیزی هر به‌روزرسانی افزایشی ضمیمه‌شده پس از امضا را ثبت می‌کند

هنگام انجام این حساب به integer width توجه کنید. GetSignProcessByteRange در flat API یک Integer ۳۲بیتی برمی‌گرداند، اما valueهای زیرین Int64 هستند، پس روی یک file بالای ۲ GB flat accessor بی‌صدا truncate می‌کند. به سراغ TPDFlibSigner.GetByteRange در لایهٔ class بروید، که Int64 برمی‌گرداند، یا valueها را از GetSignatureValueByName همان‌طور که code audit بالا می‌کند parse کنید

آنچه کتابخانه به شما می‌سپارد

دو مرز بهتر در design time یاد گرفته شوند تا در sprint نهایی. flat API در TPDFlib اصلاً wrapper signature-verification حمل نمی‌کند. cryptographic verification یک لایه پایین‌تر زندگی می‌کند، در TPDFlibSignatureVerifier، که VerifySignature آن valid، invalid یا unknown پاسخ می‌دهد. همچنین هیچ HTTP client داخلی برای RFC 3161 timestamp authorityها نیست. کتابخانه hash برای submit را محاسبه می‌کند و CMS augmented را یک‌بار token برگشت re-embed می‌کند، اما network round trip به TSA را شما باید بنویسید. هر دو wrap کردنشان سرراست است و پیدا کردنشان یک هفته قبل از release به‌طرز واقعی ناخوشایند است، پس آن‌ها را از اولین sketch طراحی کنید

یک سوال درباره compliance ارزش آن دارد plainly حل شود، چون تصمیم می‌گیرد آخرین gate کجا می‌رود: آیا افزودن یک signature PDF/A را می‌شکند؟ نه به‌تنهایی. signature به‌عنوان یک incremental update می‌رسد، و ISO 19005-2 به بعد صریحاً documentهای signed را اجازه می‌دهد. catch در signature appearance است، که بر اساس همان ruleهایی بازی می‌کند که هر content page دیگر، شامل fontهای embedded و هیچ color device-dependentی. پس gate نهایی در workbench یک preflight run دیگر است، این بار در برابر output signed. CheckFileCompliance را به‌عنوان check سریع در-pipeline در نظر بگیرید و همچنان release candidateها را با یک ابزار مستقل مانند veraPDF verify کنید، چون validatorها rule setهای هم‌پوشان اما غیریکسان implement می‌کنند؛ وقتی دو تا مخالفت می‌کنند، text finding معمولاً clause برای خواندن را name می‌کند

یک نکته sequencing از همه این‌ها بیرون می‌افتد. signing و timestamp یک pass واحد نیستند: signature baseline اول نوشته می‌شود، سپس یک process timestamp مجزا CMS را داخل فضای /Contents reserved augment می‌کند، که دقیقاً چرایی آن است که line reserve-bytes قبل‌تر آن‌قدر وزن داشت. برای لایه‌های timestamp و long-term validation که روی این workbench build می‌شوند، walkthrough signing و validation در PAdES signature را از baseline تا B-LT حمل می‌کند، و نیمه preflight در راهنمای preflight در PDF/A و PDF/UA عمیق‌تر می‌رود. مستندات کامل API و downloadهای trial در page محصول PDF Library for Delphi زندگی می‌کنند