مقاله فنی

امضا کردن فایل‌های PDF با PAdES B-B در دلفی با استفاده از PDFium

کامپوننت PDFium یک فایل PDF را از طریق متد SignPades خود با امضای دیجیتال PAdES B-B امضا می‌کند: سند را بارگذاری می‌نماید، محدوده بایت‌های امضاشده را هش می‌کند، ساختار CAdES CMS را می‌سازد، و امضا را به عنوان یک به‌روزرسانی افزایشی (incremental update) به فایل ضمیمه می‌کند. پس‌زمینه رمزنگاری فقط مخصوص ویندوز است، بنابراین قبل از امضا کردن، هر فراخوانی را با ویژگی PadesCryptoAvailable محافظت کنید

موقعیت آشنایی است. یک فایل PDF مربوط به قرارداد به دست شما می‌رسد، بخش حقوقی می‌خواهد آن را قبل از ارسال به صورت دیجیتالی امضا کنید، و شما به سراغ همان نسخه از پی‌دی‌اف‌یوم می‌روید که قبلاً برای رندر و بررسی اسناد استفاده می‌کردید، اما متوجه می‌شوید که پی‌دی‌اف‌یوم اصلاً نمی‌تواند امضایی بنویسد. API امضای آن کاملاً فقط‌خواندنی است. کامپوننت PDFium این خلاء را با در اختیار گرفتن کل خط لوله امضا در پاسکال، از تابع هش گرفته تا تزریق در سطح بایت، پر می‌کند و این مقاله این مسیر را از ابتدا تا انتها بررسی می‌نماید

چرا پی‌دی‌اف‌یوم (PDFium) نمی‌تواند امضای دیجیتال بنویسد؟

پی‌دی‌اف‌یوم امضاها را به صورت شیء فقط‌خواندنی ارائه می‌دهد و ابزاری برای ایجاد آنها ندارد. متدهای خانواده FPDFSignatureObj_* به شما اجازه می‌دهند امضاهای موجود را شمارش کنید، بخش /Contents آن را بخوانید، و /ByteRange آن را بررسی نمایید، اما هیچ متد معادلی وجود ندارد که دیکشنری امضا بسازد، فضایی را برای /Contents رزرو کند، یا محدوده بایت را بنویسد؛ قابلیت ذخیره‌سازی افزایشی وجود دارد (متد FPDF_SaveAsCopy با فلگ FPDF_INCREMENTAL) اما هیچ ارتباطی به امضا کردن ندارد. بنابراین، هر کامپوننتی که قرار است فایل PDF را بر پایه پی‌دی‌اف‌یوم امضا کند، باید تمام بایت‌های امضا را خودش تولید کند، به همین دلیل کامپوننت PDFium این سازوکار را در قالب سه یونیت پاسکال خالص ارائه کرده است. کامپایلر FPC 3.2.2 توابع md5 و sha1 را دارد اما فاقد SHA-2 است، و API مربوط به SHA-256 در یونیت System.Hash دلفی نیز با FPC سازگار نیست، بنابراین یونیت FPdfSha256 یک پیاده‌سازی مستقل از FIPS 180-4 است که تمام مسیرهای کد CMS را روی یک نوع TSHA256Digest و بدون هیچ شاخه‌بندی کامپایلر نگه‌می‌دارد. یونیت FPdfAsn1 رمزگذار و خواننده DER مورد نیاز ساختارهای CMS را فراهم می‌کند، و FPdfCms ساختار CAdES SignedData را بر پایه هر دو می‌سازد

چگونه یک PDF را در دلفی به صورت دیجیتالی امضا کنیم؟

سند را بارگذاری کنید، سپس متد SignPades را با اثر انگشت گواهینامه (certificate thumbprint) فراخوانی نمایید. کامپوننت PDFium آن اثر انگشت را در مخزن گواهینامه «MY» کاربر فعلی (Current User) جستجو کرده، گواهینامه منطبق و کلید خصوصی آن را استخراج می‌کند و یک نسخه امضاشده را در مسیری که نام برده‌اید می‌نویسد

uses
  PDFium, FPdfCrypto;

procedure SignContract(const AThumbprint: string);
var
  Pdf: TPdf;
begin
  if not PadesCryptoAvailable then
    raise Exception.Create('PAdES signing requires the Windows CNG backend');

  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := 'contract.pdf';   // the document to sign
    Pdf.Active := True;
    // Second argument: SHA-1 thumbprint of a certificate in the Current
    // User "MY" store. First argument: destination for the signed copy.
    if not Pdf.SignPades('contract-signed.pdf', AThumbprint) then
      raise Exception.Create('Signing failed');
  finally
    Pdf.Free;
  end;
end;

چه چیزهایی در CMS قرار می‌گیرند: ویژگی‌های امضاشده و RFC 5652

یک امضای پایه PAdES یک امضای ساده RSA روی کل فایل نیست؛ بلکه یک ساختار CAdES CMS SignedData حاوی مجموعه‌ای الزامی از ویژگی‌های امضاشده (signed attributes) است، و متد FPdfCms.BuildSignedData دقیقاً همان مجموعه را تولید می‌کند: شامل content-type، message-digest، و signing-certificate-v2، یعنی ویژگی ESS که امضا را بر اساس هش به گواهینامه امضاکننده متصل می‌کند. یک جزئیات در اینجا تقریباً تمام پیاده‌سازی‌های دستی CMS را با مشکل مواجه می‌کند. استاندارد RFC 5652 §5.4 ایجاب می‌کند خلاصه ویژگی‌های امضاشده روی انکودینگ DER SET OF با تگ 0x31 محاسبه شود، در حالی که همان ویژگی‌ها در داخل SignerInfo در زیر تگ IMPLICIT [0] یعنی 0xA0 قرار دارند. کامپوننت PDFium مجموعه ویژگی‌ها را یک بار رمزگذاری می‌کند، هش فرمت 0x31 را محاسبه می‌نماید، سپس برای ارسال فقط بایت تگ آغازین را به 0xA0 تغییر می‌دهد، به طوری که یک بافر برای هر دو نقش و بدون نیاز به پیمایش مجدد درخت استفاده شود

var
  Pdf: TPdf;
  Opts: TPadesSignOptions;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := 'contract.pdf';
    Pdf.Active := True;

    Opts := TPadesSignOptions.Default;
    Opts.CertificateThumbprint := 'a1b2c3d4e5f6...';  // signer in the MY store
    Opts.Reason := 'I approve this agreement';
    Opts.Location := 'Berlin, DE';
    Opts.ContentsSize := 16384;                        // hex width of /Contents

    if not Pdf.SignPades('contract-signed.pdf', Opts) then
      raise Exception.Create('Signing failed');
  finally
    Pdf.Free;
  end;
end;

اورلودهای متد با پارامترهای اختیاری، متادیتای دیکشنری امضا را که استاندارد ISO 32000-1 §12.8.1 تعریف کرده است اضافه می‌کنند: شامل فیلدهای Reason، Location، ContactInfo، و Name، که همگی اختیاری بوده و در دیکشنری مقدار امضا نوشته می‌شوند. یک محدودیت وجود دارد که ممکن است با آن مواجه شوید. اگر ویژگی CommitmentTypeOid را برای افزودن ویژگی امضاشده جهت نشان دادن نوع تعهد (commitment-type-indication) تحت استاندارد CAdES تنظیم می‌کنید، فیلد Reason را به طور همزمان تنظیم نکنید؛ بند ۶.۳ استاندارد ETSI EN 319 142-1 حمل هر دو را ممنوع کرده است، زیرا این دو فیلد یک هدف یکسان را با روش‌های متفاوت بیان می‌کنند

چگونه ByteRange و بخش /Contents با هم هماهنگ می‌شوند؟

یک امضا باید کل فایل را به جز بایت‌هایی که خود امضا را نگه می‌دارند، پوشش دهد، و استاندارد PAdES این چرخه بی‌پایان را با یک فیلد نگهدارنده با عرض ثابت که متد SignPadesBytes به دقت آن را مدیریت می‌کند، حل می‌نماید. این متد یک رشته هگز /Contents با عرض ContentsSize بایت (به طور پیش‌فرض ۱۶۳۸۴ بایت، که به راحتی بزرگ‌تر از یک CMS SignedData معمولی است) رزرو می‌کند، به‌روزرسانی افزایشی را برای یافتن آفست دقیق بخش سریال‌سازی می‌نماید، سپس /ByteRange را به عنوان دو بازه که بخش رزرو شده را در بر می‌گیرند (هر چیزی قبل از جداکننده شروع رشته هگز و هر چیزی بعد از جداکننده پایان آن) محاسبه می‌کند. فرآیند SHA-256 تنها روی این دو بازه اجرا می‌شود. ساختار نهایی CMS به صورت هگز در بخش رزرو شده رمزگذاری می‌شود، با صفر پر می‌شود تا به اندازه ثابت برسد، و به‌روزرسانی جدول ارجاع متقابل (cross-reference) ضمیمه می‌گردد. از آنجا که عرض بخش از ابتدا ثابت است، پر کردن آن باعث جابه‌جایی بایت‌های امضاشده در فایل نمی‌شود، که دلیل معتبر ماندن محدوده بایت است؛ بایت‌های سند اصلی به طور دقیق حفظ می‌شوند، بنابراین امضای قبلی روی همان فایل کاملاً دست‌نخورده باقی می‌ماند، دقیقاً همان‌طور که امضای افزایشی استاندارد ISO 32000-1 §12.8.1 نیاز دارد

پس‌زمینه Windows CNG و محدودیت‌های آن

کامپوننت PDFium تنها روی سیستم‌عامل ویندوز عملیات امضا را انجام می‌دهد، و این مرز آگاهانه تعیین شده است. یونیت FPdfCryptoWin فایل‌های crypt32.dll و ncrypt.dll را به صورت پویا متصل می‌کند و هیچ وابستگی زمان کامپایل به DLL اضافه نمی‌نماید، و زنجیره امضا بر اساس ساختار استاندارد CNG است: باز کردن مخزن MY، یافتن گواهینامه با هش، دریافت هندل کلید خصوصی از طریق CryptAcquireCertificatePrivateKey، و فراخوانی NCryptSignHash. روش‌های رمزنگاری RSA با PKCS#1 v1.5، RSA-PSS و ECDSA همگی پشتیبانی می‌شوند. الگوریتم ECDSA نیاز به یک اصلاح دارد که بقیه روش‌ها ندارند، زیرا NCryptSignHash جفت خام r-and-s تحت استاندارد IEEE P1363 را برمی‌گرداند در حالی که CMS انتظار یک توالی DER ECDSA-Sig-Value را دارد، بنابراین بخش پس‌زمینه آن را بر اساس استاندارد RFC 5480 دوباره رمزگذاری می‌کند

var
  Pdf: TPdf;
  Opts: TPadesSignOptions;
  Output: TFileStream;
begin
  if not PadesCryptoAvailable then
    Exit;   // no signing backend on this platform

  Opts := TPadesSignOptions.Default;
  Opts.CertificateThumbprint := ReadThumbprintFromConfig;

  Pdf := TPdf.Create(nil);
  Output := TFileStream.Create('contract-signed.pdf', fmCreate);
  try
    Pdf.FileName := 'contract.pdf';
    Pdf.Active := True;
    Pdf.SignPadesToStream(Output, Opts);
  finally
    Output.Free;
    Pdf.Free;
  end;
end;

پیامد عملی این است که کلید خصوصی باید در مخزن گواهینامه ویندوز قرار داشته باشد. گواهینامه‌ای که در یک فایل PFX نگهداری می‌شود، تنها پس از وارد کردن آن به مخزن کاربر فعلی (Current User) کار می‌کند، که در این حالت اثر انگشت (thumbprint) آن همان مقداری است که به SignPades ارسال می‌کنید. این نسخه فاقد مسیرهای PKCS#11 یا HSM و بدون پس‌زمینه فایل کلید نرم‌افزاری است، بنابراین وقتی ویژگی PadesCryptoAvailable مقدار False را برمی‌گرداند، امضا در آن سیستم انجام‌شدنی نخواهد بود

جایی که PAdES B-B متوقف می‌شود

سطح PAdES B-B پایه و حداقل سطح از سطوح چهارگانه PAdES است: این سطح ثابت می‌کند چه کسی امضا کرده و بایت‌ها از آن زمان تغییری نکرده‌اند، و نه هیچ چیز فراتر از آن. یک امضای B-B فاقد مهر زمانی معتبر است، بنابراین نمی‌تواند ثابت کند که امضا چه زمانی رخ داده است، و هیچ اطلاعات ابطالی را تعبیه نمی‌کند، در نتیجه ارزیابی که سال‌ها بعد آن را بررسی می‌کند، باید خودش زنجیره گواهینامه و وضعیت آن را دریافت نماید. خلاءهای تعبیه شده در سطح پایه دقیقاً همان مواردی هستند که سطوح بالاتر برطرف می‌کنند. هنگامی که به زمان امضایی نیاز دارید که بازرس آن را بپذیرد، افزودن مهر زمانی RFC 3161 و DSS برای اعتبارسنجی بلندمدت امضا را به سطح B-T و بالاتر ارتقا می‌دهد؛ وقتی می‌خواهید یک امضای نهایی را بخوانید و تأیید کنید که به کدام سطح رسیده است، بخش بررسی امضای PDF و سطح PAdES آن ابزار مناسب شماست؛ و قبل از امضا کردن هر چیزی، ممیزی فایل PDF برای ریسک‌های امنیتی به شما می‌گوید که قرار است نام خود را روی چه چیزی بنویسید

متدهای SignPades نشان داده شده در اینجا همراه با PDFium Component برای دلفی و سی‌پلاس‌پلاس‌بیلدر ارائه می‌شوند، در کنار سیستم بررسی فقط‌خواندنی امضا که پی‌دی‌اف‌یوم به طور پیش‌فرض فراهم می‌کند