مقاله فنی

راستی‌آزمایی امضای دیجیتال PDF در دلفی با HotPDF

HotPDF امضاهای دیجیتال را در سندهای PDF بارگذاری‌شده از راه سه متد THotPDF راستی‌آزمایی می‌کند: GetLoadedSignatureInfo، VerifyLoadedSignature و VerifyLoadedSignatureEx که در v2.259.0 معرفی شدند. کامپوننت قطعه‌های /ByteRange فایل اصلی را دوباره هش می‌کند، صفت messageDigest در CMS را بررسی می‌کند، و یک راستی‌آزمایی RSA PKCS#1 v1.5 را در برابر گواهی امضاکننده جاسازی‌شده اجرا می‌کند، و وقتی بایت‌های سند دست‌نخورده باشند svValid برمی‌گرداند

سناریو پیش‌پاافتاده است و آنچه در خطر است نه. طرف مقابل قراردادی امضاشده را برمی‌گرداند، جریان کاری شما باید بایگانی‌اش کند، و کسی تنها پرسش مهم را می‌پرسد: آیا این همان سندی است که فرستادیم، بایت‌به‌بایت، امضاشده با همان گواهی‌ای که ادعا می‌کند؟ پاسخ‌دادن به این پرسش در کد، سمت راستی‌آزمایی داستان امضاست؛ سمت امضا، یعنی ساختن و جاسازی امضاهای PAdES از ابتدا، در مقاله همراه درباره ساخت امضاهای دیجیتال PAdES با HotPDF پوشش داده شده است. این مقاله درباره جهت دیگر است: یک PDF از پیش امضاشده می‌رسد و شما به‌جای اسکرین‌شات از علامت تیک سبز Acrobat، حکمی برنامه‌نویسی‌شده می‌خواهید

یک PDF امضاشده چگونه اثبات می‌کند دستکاری نشده است؟

امضای PDF از بازه‌های بایتی مشخصی از فایل محافظت می‌کند، نه از مفهومی انتزاعی به نام «سند». استاندارد ISO 32000-1 §12.8 این سازوکار را تعریف می‌کند: فیلد فرم امضا دیکشنری‌ای دارد که درایه /Contents آن یک ظرف CMS SignedData (بر پایه RFC 5652) را نگه می‌دارد، و آرایه /ByteRange آن طبق §12.8.1 دقیقاً ناحیه‌هایی از فایل را نام می‌برد که امضا پوششش می‌دهد. این آرایه فهرستی از جفت‌های افست و طول است، در عمل دو قطعه: هرچه پیش از رشته هگز /Contents است، و هرچه پس از آن. مقدار امضا نمی‌تواند خودش را بپوشاند، پس فایل حول همان حفره هش می‌شود

آن طراحی پیامدی دارد که شکل کل API را می‌سازد: راستی‌آزمایی باید بایت‌های سریال‌شده اصلی را هش کند، دقیقاً همان‌طور که روی دیسک نشسته‌اند. یک مدل شیء تجزیه‌شده به این کار نمی‌آید، چون سریال‌سازی دوباره حتی برای سندی تغییرنیافته هم بایت‌های متفاوتی تولید می‌کند. پس HotPDF در برابر همان فایل مبدأیی راستی‌آزمایی می‌کند که سند از آن بارگذاری شده، یا در برابر یک TStream از بایت‌های خام که خودتان می‌دهید، و هرگز در برابر بازنمایی درون‌حافظه‌ای‌اش

دیاگرام راستی‌آزمایی یک PDF امضاشده با HotPDF در دلفی از راه هش دوباره دو قطعه ByteRange از فایل مبدأ حول حفره Contents، در حالی که مدل تجزیه‌شده درون حافظه هرگز هش نمی‌شود چون سریال‌سازی دوباره بایت‌ها را تغییر می‌دهد
راستی‌آزمایی دو قطعه ByteRange از بایت‌های مبدأ را دقیقاً به همان شکل سریال‌شده هش می‌کند؛ مدل تجزیه‌شده درون حافظه به کار نمی‌آید چون سریال‌سازی دوباره حتی برای سندی تغییرنیافته بایت‌های متفاوتی تولید می‌کند

خواندن فراداده امضا پیش از راستی‌آزمایی هر چیزی

GetLoadedSignatureInfo دیکشنری امضا و ظرف CMS آن را بی‌آنکه به حتی یک بایت سند دست بزند تجزیه می‌کند، و همین آن را نخستین فراخوانی درست می‌کند وقتی فقط می‌خواهید نشان دهید چه کسی و کِی امضا کرده است. فیلدهای امضا از 0 و به ترتیب فیلدهای فرم اندیس می‌شوند، و GetLoadedSignatureFieldCount می‌گوید چند تا وجود دارد. رکورد بازگشتی THPDFSignatureInfo نام فیلد، /SubFilter، نام مشترک گواهی امضاکننده، نام‌های متمایز موضوع و صادرکننده، شماره سریال، تاریخ‌های اعتبار، زمان امضا (از صفت امضاشده در صورت وجود، وگرنه از درایه /M دیکشنری)، نام الگوریتم چکیده، و رشته‌های /Reason، /Location و /ContactInfo را حمل می‌کند. عضو Status آن روی svNotVerified می‌ماند، برچسبی صادقانه برای «تجزیه شد، بررسی نشد»

var
  Pdf: THotPDF;
  Info: THPDFSignatureInfo;
  I: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.LoadFromFile('signed-contract.pdf');
    for I := 0 to Pdf.GetLoadedSignatureFieldCount - 1 do
    begin
      Info := Pdf.GetLoadedSignatureInfo(I);
      Writeln('Field:     ', Info.FieldName);
      Writeln('Signer:    ', Info.SignerName);
      Writeln('Issuer:    ', Info.IssuerDN);
      Writeln('Algorithm: ', Info.HashAlgorithm);
      Writeln('SubFilter: ', Info.SubFilter);
    end;
  finally
    Pdf.Free;
  end;
end;

اجرای بررسی رمزنگاشتی

VerifyLoadedSignatureEx راستی‌آزمایی کامل را برای سندی که از فایل بارگذاری شده انجام می‌دهد و رکورد اطلاعات پرشده را در همان یک فراخوانی برمی‌گرداند: فایل مبدأ را دوباره باز می‌کند، قطعه‌های /ByteRange را با الگوریتم چکیده SignerInfo هش می‌کند، نتیجه را با صفت امضاشده messageDigest مقایسه می‌کند (RFC 5652 §5.4)، و بعد امضا را روی کدگذاری دوباره DER SET صفت‌های امضاشده با RSA راستی‌آزمایی می‌کند. وقتی امضایی هیچ صفت امضاشده‌ای نداشته باشد، بررسی RSA به‌جای آن مستقیم روی هش سند اجرا می‌شود. امضاهای پشتیبانی‌شده RSA PKCS#1 v1.5 با چکیده‌های SHA-1، SHA-256، SHA-384 یا SHA-512 هستند، که زیرفیلترهای adbe.pkcs7.detached و ETSI.CAdES.detached تولیدشده با ابزارهای امضای رایج را پوشش می‌دهد

خط لوله VerifyLoadedSignatureEx در HotPDF برای دلفی: باز کردن دوباره فایل مبدأ، هش قطعه‌های ByteRange، مقایسه با صفت امضاشده messageDigest در CMS، سپس راستی‌آزمایی RSA PKCS#1 v1.5 که svValid، svDigestMismatch یا svSignatureInvalid تولید می‌کند
VerifyLoadedSignatureEx قطعه‌های ByteRange را دوباره هش می‌کند، آن‌ها را با صفت امضاشده messageDigest مقایسه می‌کند، و پیش از گزارش svValid یا یک وضعیت شکست مشخص، DER SET صفت‌های امضاشده را با RSA راستی‌آزمایی می‌کند
var
  Status: THPDFSignatureVerifyStatus;
  Info: THPDFSignatureInfo;
begin
  Status := Pdf.VerifyLoadedSignatureEx(0, Info);
  case Status of
    svValid:
      if Info.CoversWholeDocument then
        Writeln('Valid; signature covers the whole file')
      else
        Writeln('Valid; file was extended after signing');
    svDigestMismatch:
      Writeln('Document bytes changed after signing');
    svSignatureInvalid:
      Writeln('RSA check failed over signed attributes');
    svUnsupportedAlgorithm:
      Writeln('Non-RSA key or unknown digest algorithm');
    svMalformed:
      Writeln('CMS container could not be parsed');
    svSourceUnavailable:
      Writeln('No source bytes; use the TStream overload');
  end;
end;

دو جزئیات پیاده‌سازی ارزش دانستن دارند، چون شکست‌هایی را توضیح می‌دهند که از بیرون مرموز به نظر می‌رسند. نخست، بررسی صفت‌های امضاشده درباره کدگذاری وسواسی است: درون فایل این صفت‌ها با برچسب [0] IMPLICIT می‌آیند، اما امضا روی شکل SET OF از نوع DER آن‌ها محاسبه شده، پس راستی‌آزما پیش از هش‌کردن دوباره برچسب می‌زند، دقیقاً همان‌طور که RFC 5652 §5.4 می‌خواهد. راستی‌آزمای دست‌سازی که بایت‌ها را همان‌گونه که در فایل ظاهر می‌شوند هش کند، هر سند درست‌امضاشده‌ای را رد خواهد کرد. دوم، /Contents طبق قرارداد تا یک بودجه بایتی رزروشده با صفر پر می‌شود، پس راستی‌آزما پیش از تجزیه، بلاب DER را به طول واقعی SEQUENCE بیرونی‌اش کوتاه می‌کند؛ صفرهای انتهایی که آشغال به نظر می‌رسند عادی‌اند نه خرابی. همین خانواده از خطرهای تجزیه ASN.1، در سمت واردکردن گواهی، موضوع مقاله سخت‌سازی امنیتی PKCS#12 و ASN.1 در HotPDF است

یک امضای معتبر واقعاً چه چیزی را تضمین می‌کند؟

svValid دقیقاً یعنی این: بایت‌هایی که /ByteRange نامشان برده به همان مقداری هش می‌شوند که امضاکننده امضا کرده، و امضا زیر کلید عمومی گواهی جاسازی‌شده در ظرف CMS راستی‌آزمایی می‌شود. یعنی درستی بایت‌ها به‌علاوه پیوند کلید، و نه چیزی بیشتر. راستی‌آزمایی زنجیره گواهی و اعتماد صریحاً بیرون از دامنه راستی‌آزمای HotPDF است: زنجیره را تا ریشه نمی‌پیماید، ابطال را بررسی نمی‌کند، و به هیچ انبار اعتمادی مراجعه نمی‌کند. گواهی خودامضای مهاجمی که سندی تغییریافته را دوباره امضا کرده، به‌عنوان svValid راستی‌آزمایی خواهد شد، چون ریاضیات از درون سازگار است. اینکه امضاکننده همانی است که ادعا می‌کند یا نه، و اینکه کسی باید به او اعتماد کند یا نه، تصمیمی سیاستی است که به لایه‌ای جداگانه تعلق دارد، چه فهرست سفید گواهی‌های سازمان شما باشد، چه انبار گواهی ویندوز، چه یک مرجع اعتبارسنجی

پرچم CoversWholeDocument از شکافی ظریف‌تر پاسبانی می‌کند. هر امضا فقط /ByteRange خودش را می‌پوشاند، و سازوکار به‌روزرسانی افزایشی در PDF اجازه می‌دهد پس از یک امضا محتوا افزوده شود بی‌آنکه امضا باطل شود، که عمدی است و جریان‌های کاری چندامضایی به همین شکل کار می‌کنند. این پرچم هنگام راستی‌آزمایی محاسبه می‌شود و فقط وقتی درست است که دو قطعه به‌علاوه شکاف /Contents کل فایل را در بر بگیرند. وقتی svValid همراه با CoversWholeDocument نادرست می‌رسد، بازنگری امضاشده دست‌نخورده است اما فایل افزوده‌های بعدی دارد، و اینکه آن افزوده‌ها چه تغییری داده‌اند چیزی است که جریان کاری شما باید درباره تحملش تصمیم بگیرد

دیاگرام دامنه آنچه svValid در راستی‌آزمایی امضا با HotPDF تضمین می‌کند: درستی بایت‌های ByteRange و پیوند کلید، در حالی که پیمایش زنجیره، ابطال و انبارهای اعتماد بیرون از دامنه می‌مانند و CoversWholeDocument به‌روزرسانی‌های افزایشی الحاقی را علامت می‌زند
مقدار svValid یعنی درستی بایت‌ها به‌علاوه پیوند کلید و نه بیشتر؛ پیمایش زنجیره، ابطال و انبارهای اعتماد به لایه سیاستی جداگانه‌ای تعلق دارند، و CoversWholeDocument=false نشانه بایت‌های افزوده‌شده پس از امضاست

سندهای بارگذاری‌شده از جریان و سندهای رمزنگاری‌شده بایت‌های مبدأ خودشان را می‌خواهند

نسخه‌های بی‌پارامتر VerifyLoadedSignature و VerifyLoadedSignatureEx به این وابسته‌اند که کامپوننت به یاد داشته باشد سند از کدام فایل آمده است. سند را از یک جریان بارگذاری کنید و دیگر نام فایلی برای باز کردن دوباره نیست؛ همین درباره مسیر بارگذاری دوباره با گذرواژه که برای سندهای رمزنگاری‌شده به کار می‌رود هم صادق است، همان جریان کاری که در مقاله رمزنگاری AES-256 در PDF با HotPDF شرح داده شده. در هر دو حالت، نسخه‌های فایل‌محور به‌جای حدس‌زدن مقدار svSourceUnavailable برمی‌گردانند. راه‌حل نسخه TStream است که می‌گذارد بایت‌های خام اصلی را از هرجا که نگهشان داشته‌اید تحویل دهید: فایلی که هنوز دارید، یک بافر حافظه، یا یک بلاب پایگاه داده

var
  Src: TFileStream;
  Status: THPDFSignatureVerifyStatus;
  Info: THPDFSignatureInfo;
begin
  // سند بارگذاری‌شده از جریان: کامپوننت نام فایل مبدأ را
  // ندارد، پس بایت‌های اصلی را خودتان بدهید.
  Src := TFileStream.Create('signed-contract.pdf',
    fmOpenRead or fmShareDenyWrite);
  try
    Status := Pdf.VerifyLoadedSignature(0, Src, Info);
    if Status <> svValid then
      Writeln('Verification failed: ', Ord(Status));
  finally
    Src.Free;
  end;
end;

گزارش‌دادن آنچه نمی‌توانید راستی‌آزمایی کنید

راستی‌آزمایی که فقط «معتبر» و «نامعتبر» می‌شناسد، سندهایی را که صرفاً نمی‌فهمد غلط گزارش می‌کند، پس شمارشی وضعیت‌ها همان حالت‌هایی را جدا می‌کند که رابط کاربری شما باید تمیزشان دهد. svDigestMismatch یعنی بایت‌های سند پس از امضا تغییر کرده‌اند، همان علامت کلاسیک دستکاری. svSignatureInvalid یعنی بایت‌ها درست هش می‌شوند اما بررسی RSA شکست خورده، که به مقدار امضای خراب یا جعلی اشاره می‌کند. svUnsupportedAlgorithm پاسخ صادقانه برای کلیدهای ECDSA و چکیده‌های ناشناخته است: ممکن است امضا کاملاً سالم باشد، HotPDF فقط نمی‌تواند بررسی‌اش کند، و گزارش آن به‌عنوان «نامعتبر» به سندی سالم تهمت می‌زند. svMalformed ظرف CMS را علامت می‌زند که اصلاً قابل تجزیه نبوده است. برای بررسی‌های دروازه‌ای، VerifyAllLoadedSignatures فقط وقتی درست برمی‌گرداند که دست‌کم یک فیلد امضا وجود داشته باشد و هر کدامشان svValid راستی‌آزمایی شود، یک بولی واحد و راحت برای خط لوله ورودی بایگانی که کمتر از این را نمی‌پذیرد

راستی‌آزمایی امضا، امضای PAdES، رمزنگاری AES-256 و رابط ویرایش سند بارگذاری‌شده همگی در همان کتابخانه بومی VCL برای دلفی و C++Builder عرضه می‌شوند، بدون وابستگی به DLL بیرونی؛ فهرست کامل قابلیت‌ها و نسخه‌های پشتیبانی‌شده IDE در صفحه محصول HotPDF Delphi Component آمده است