مقاله فنی

چرا اعتبارسنج‌ها امضاهای PAdES را رد می‌کنند: PDFium در دلفی

یک اعتبارسنج امضای PAdES را تقریباً در هر موردی که با کامپوننت PDFium در دلفی دیباگ کرده‌ایم به یکی از سه دلیل رد می‌کند: آرایه /ByteRange هنوز نگه‌دارنده های صفر خود را دارد، زمان امضای /M یک رشته تاریخ PDF با ساختار صحیح نیست، یا یک به‌روزرسانی افزایشی ورودی /Encrypt را از یک سند رمزگذاری‌شده حذف کرده است. هر سه مورد فایل‌هایی تولید می‌کنند که به خوبی باز می‌شوند، به خوبی رندر می‌گردند و در لحظه‌ای که یک اعتبارسنج استاندارد دیکشنری امضا را می‌خواند با شکست مواجه می‌شوند

سناریویی که انگیزه این مقاله را ایجاد می‌کند به طرز دردناکی خاص است. شما قراردادی را با جریان کاری مقاله امضای PDFها با PAdES B-B امضا می‌کنید، کد بازرسی خودتان گزارش می‌دهد که امضا وجود دارد و از نظر ساختاری سالم است، هر تست محلی سبز است — و سپس طرف مقابل فایل را در پلتفرم اعتبارسنجی خود آپلود می‌کند و با یک ضربدر قرمز مواجه می‌شود. هیچ چیز در مورد این خرابی در نمایشگر قابل مشاهده نیست، زیرا هیچ‌کدام از این سه نقص با محتوای صفحه برخورد ندارند. آنها کاملاً در دیکشنری امضا و انتهای فایل زندگی می‌کنند، که دقیقاً همان جایی است که اعتبارسنج‌ها نگاه می‌کنند و نمایشگرها بیشتر اوقات نگاه نمی‌کنند

چرا محدوده بایت (ByteRange) امضای PDF من نامعتبر است؟

رد شدن ByteRange تقریباً همیشه به این معنی است که چهار فیلد هرگز پر نشده‌اند، نه اینکه محدوده‌ها به طور جزئی اشتباه باشند. آرایه /ByteRange [A B C D] دو بازه را اعلام می‌کند، بایت‌های A تا A+B و بایت‌های C تا C+D، و استاندارد EN 319 142-1 §6.3 (شرط k) ایجاب می‌کند که آنها با هم کل فایل را به جز رشته هگز رمزگذاری‌شده /Contents پوشش دهند. در اعداد: A برابر ۰ است، C >= A+B است، C+D برابر با طول فایل است و فاصله بین B و C دقیقاً رشته هگز <...> را نگه می‌دارد — دو بایت براکت به علاوه دو کاراکتر هگز به ازای هر بایت از داده‌های CMS. اعتبارسنجی که مقدار [0 0 0 0] را می‌خواند نتیجه می‌گیرد که امضا هیچ چیز را پوشش نمی‌دهد و آن را رد می‌کند، بدون توجه به اینکه ساختار CMS در داخل /Contents چقدر صحیح است

مکانیسم نحوه وقوع این اتفاق ارزش درک دارد زیرا همان الگو در هر سیستم امضایی وجود دارد. یک امضاکننده نمی‌تواند آفست‌های نهایی را تا زمانی که فایل چیده نشده بداند، بنابراین نویسنده آرایه را با نگه‌دارنده‌های صفر با عرض ثابت صادر می‌کند و پس از چیدمان آنها را پر می‌کند. در نسخه‌های کامپوننت PDFium قبل از ۲.۱۴.۱، آن پر کردن مجدد به دنبال الگوی نگه‌دارنده از موقعیت /Contents می‌گشت — اما /ByteRange قبل از /Contents در دیکشنری قرار دارد، بنابراین جستجو چیزی پیدا نمی‌کرد و هر سه جایگزینی بی سر و صدا با شکست مواجه می‌شدند. دایجست CMS روی محدوده‌های صحیح محاسبه می‌شد، بنابراین رمزنگاری سالم بود; اما اعلام آن محدوده‌ها روی صفر باقی می‌ماند، بنابراین هر اعتبارسنج استانداردی فایل را رد می‌کرد. مهرهای زمان سند PAdES B-LTA که از همان چیدمان دیکشنری استفاده می‌کنند نیز به همین ترتیب با شکست مواجه می‌شدند — که اگر بر اساس امضاهای طولانی‌مدت B-LT و B-LTA توسعه می‌دهید این موضوع مرتبط است. نسخه ۲.۱۴.۱ مقادیر را از شروع شیء امضا پر می‌کند و این اصلاح با یک تست رگرسیون پوشش داده می‌شود که فایل تولید شده را تجزیه کرده و محاسبات زیر را ادعا می‌نماید

function SignedByteRangeCoversFile(const FileName: string): Boolean;
var
  Raw: TBytes;
  Text: AnsiString;
  P, N: Integer;
  F: array[0..3] of Int64;
begin
  Raw := TFile.ReadAllBytes(FileName);
  SetString(Text, PAnsiChar(@Raw[0]), Length(Raw));
  P := Pos('/ByteRange', Text);          // first signature only
  Result := P > 0;
  if not Result then Exit;
  Inc(P, Length('/ByteRange'));
  for N := 0 to 3 do
  begin
    while (P <= Length(Text)) and not (Text[P] in ['0'..'9']) do Inc(P);
    F[N] := 0;
    while (P <= Length(Text)) and (Text[P] in ['0'..'9']) do
    begin
      F[N] := F[N] * 10 + Ord(Text[P]) - Ord('0');
      Inc(P);
    end;
  end;
  // EN 319 142-1 §6.3 req k: spans cover everything except /Contents
  Result := (F[0] = 0) and (F[2] >= F[0] + F[1]) and
            (F[2] + F[3] = Int64(Length(Raw)));
end;

بیست خط کد ساده RTL دلفی بدون فراخوانی کتابخانه، و این کد کل این کلاس از خطا را در نقطه تولید متوقف می‌کند. اگر یک ادعا (assertion) را از این مقاله نگه می‌دارید، این یکی را نگه دارید: خروجی امضا شده خود را تجزیه کنید و این سه برابری را قبل از خروج فایل از پروسه خود بررسی نمایید

چرا اعتبارسنج‌ها زمان امضای /M را به عنوان بدفرم علامت‌گذاری می‌کنند؟

اعتبارسنج‌های سخت‌گیر زمان امضایی را که یک رشته تاریخ کامل PDF نباشد رد می‌کنند و دو بخشی که اغلب وجود ندارند، پیشوند D: و نشانگر آفست UTC هستند. استاندارد ISO 32000-1 §7.9.4 فرمت تاریخ را به صورت D:YYYYMMDDHHmmSS و به دنبال آن یک آفست تعریف می‌کند — Z برای UTC، یا رابطه علامت‌دار +HH'mm' با آن. یک عبارت خالی 20260709143000 برای یک خواننده منعطف به عنوان تاریخ تجزیه می‌شود، اما اعتبارسنجی که گرامر را به طور تحت‌اللفظی اعمال می‌کند، یک رشته بدفرم را در یک فیلد با فرمت اجباری می‌بیند و امضا را علامت‌گذاری می‌نماید. نسخه‌های کامپوننت PDFium قبل از ۲.۱۴.۴ ورودی /M متدهای TPdf.SignPades و SignPadesBytes را دقیقاً به همان شکل خالی می‌نوشتند; از نسخه ۲.۱۴.۴ این ورودی پیشوند D: و نشانگر Z را حمل می‌کند، بنابراین زمان امضای اعلام شده به صورت D:20260709143000Z خوانده می‌شود

دو نکته عملی به این فیلد متصل است. اول اینکه، UTC را بنویسید و صریحاً اعلام کنید: یک مهر زمان بدون نشانگر آفست، اعتبارسنج را مجبور می‌کند تا رابطه منطقه زمانی را حدس بزند، و بخش ۷.۹.۴ با آفست به عنوان بخشی از فرمت برخورد می‌کند تا یک مورد اختیاری. دوم اینکه، به یاد داشته باشید /M چیست — زمان اعلام شده توسط امضاکننده، یعنی یک ادعا و نه یک اثبات. یک اعتبارسنج فرمت آن را در اینجا بررسی می‌کند و نه صحت آن را; زمان قابل اثبات از مهر زمان استاندارد RFC 3161 در PAdES B-T و بالاتر به دست می‌آید. فرمت‌بندی صحیح یک فیلد و اعتماد به آن تصمیمات مجزایی هستند و اعتبارسنج‌ها فقط اولی را اعمال می‌کنند

چرا متد SignPades خطای EPadesCrypto را روی یک PDF رمزگذاری‌شده ایجاد می‌کند؟

کامپوننت PDFium از امضای یک سند رمزگذاری‌شده خودداری می‌کند، زیرا جایگزین آن تولید فایلی است که خواننده‌های استاندارد در هنگام باز کردن آن را خراب می‌کنند. یک امضای PAdES به عنوان یک به‌روزرسانی افزایشی اضافه می‌شود و استاندارد ISO 32000-1 §7.5.6 ایجاب می‌کند که تریلر جدید یک بخش به‌روزرسانی، هر ورودی تریلر قبلی به جز /Prev را حمل کند — که در یک سند رمزگذاری‌شده این شامل /Encrypt می‌شود. حذف آن باعث می‌شود آخرین تریلر سند را بدون رمزگذاری اعلام کند، بنابراین یک خواننده استاندارد بدنه رمزگذاری‌شده را به عنوان متن ساده تجزیه می‌کند و اطلاعات خرابی دریافت می‌نماید. بدتر از آن، استریم‌ها و رشته‌هایی که امضاکننده اضافه می‌کند، خود باید با کلید سند رمزگذاری شوند تا قانونی باشند، کاری که یک تزریق‌کننده امضای متن ساده نمی‌تواند انجام دهد. هیچ راهی برای اضافه کردن یک امضای متن ساده معتبر به یک فایل رمزگذاری‌شده وجود ندارد، بنابراین از نسخه ۲.۱۴.۲ متدهای TPdf.SignPades، SignPadesBytes و InjectPadesDssMarkers به جای صدور یک نتیجه خراب یا غیرقابل اعتبارسنجی، خطای EPadesCrypto را ایجاد می‌کنند

ایجاد استثنا (exception) نتیجه صحیح است، بنابراین جریان کاری را در اطراف آن طراحی کنید به جای اینکه خطا را بگیرید و دوباره تلاش نمایید. ابتدا با مجوز مالک رمزگشایی کنید، کپی متن ساده را امضا کنید، و اگر کانال توزیع نیاز به رمزگذاری دارد، بپذیرید که رمزگذاری-سپس-امضا و امضا-سپس-رمزگذاری آرتیفکت‌های متفاوتی را با داستان‌های اعتبارسنجی متفاوت تولید می‌کنند. امضایی که روی بایت‌های متن ساده محاسبه شده است نمی‌تواند از رمزگذاری مجدد آن بایت‌ها جان سالم به در ببرد، بنابراین گزینه‌های صادقانه یک فایل متن ساده امضا شده یا تصمیم سیاستی مستند شده در کنار کد است

try
  if not Pdf.SignPades('contract-signed.pdf', AThumbprint) then
    Writeln('Signing reported failure');
except
  on E: EPadesCrypto do
  begin
    // e.g. 'SignPadesBytes: the source document is encrypted;
    //       remove encryption before signing'
    Writeln('Cannot sign: ', E.Message);
  end;
end;

چگونه دو باگ می‌توانند یکدیگر را تأیید کنند

نقص ByteRange تا زمانی که پنهان بود، مخفی ماند زیرا اعتبارسنج خود ما به روشی مکمل اشتباه بود و این مهم‌ترین درس قابل انتقال در این مقاله است. بررسی پوشش در ValidatePadesCompliance مستلزم آن بود که بازه دوم دقیقاً از A+B شروع شود — یک فاصله صفر — که طرح‌بندی استاندارد را که در آن فاصله رشته هگز /Contents را نگه می‌دارد، به اشتباه طبقه‌بندی می‌کرد. بنابراین اعتبارسنج داخلی دقیقاً طرح‌بندی استاندارد را رد می‌کرد و تولیدکننده داخلی هرگز طرح منطبق تولید نمی‌کرد و خط لوله‌های پایان به پایان امضا-سپس-اعتبارسنجی بر حسب تصادف سبز باقی می‌ماندند. هر باگ، مجموعه تست را از نمونه نقضی که باگ دیگر را آشکار می‌کرد محروم می‌کرد. نسخه ۲.۱۴.۱ هر دو طرف را در یک انتشار اصلاح کرد: تولیدکننده هر چهار فیلد را پر می‌کند و اعتبارسنج مقدار C >= A+B را با C+D برابر با طول فایل می‌پذیرد

اصلاح روش‌شناختی، اعتبارسنجی متقابل در برابر پیاده‌سازی است که خودتان ننوشته‌اید. یک تولیدکننده و یک اعتبارسنج که پایگاه کد، نویسنده، یا حتی فقط یک مدل ذهنی از فرمت را به اشتراک می‌گذارند، می‌توانند تا زمان نامحدودی با یک سوءتفاهم مشترک موافق باشند؛ یک اعتبارسنج مستقل این تقارن را می‌شکند. خروجی امضا شده خود را قبل از انتشار از طریق حداقل یک بررسی‌کننده انطباق خارجی اجرا کنید و یک خودبررسی ساختاری را به عنوان اولین خط سریع در بیلد نگه دارید — متد TPdf.ValidatePades که در مقاله بررسی امضا توضیح داده شده است، شکست پوشش را به عنوان یک مشکل نام‌گذاری‌شده گزارش می‌دهد

var
  R: TPadesValidationResult;
begin
  Pdf.FileName := 'contract-signed.pdf';
  Pdf.Active := True;
  R := Pdf.ValidatePades;
  if ppeiByteRangeNotCoveringFile in R.Issues then
    Writeln('ByteRange does not cover the file');
  if not R.IsCompliant then
    Writeln('Structural issues present: do not ship this file');
end;

یک چک‌لیست رد برای خروجی امضا شده

هنگامی که یک پلتفرم امضای PAdES شما را رد می‌کند، قبل از مشکوک شدن به گواهی‌ها یا زنجیره‌های اعتماد، دلایل ساختاری ارزان را بررسی کنید. آرایه /ByteRange را بخوانید و سه برابری را تأیید کنید: فیلد اول صفر، بازه دوم شروع در انتهای فیلد اول یا بعد از آن، بازه دوم خاتمه دقیقاً در انتهای فایل. ورودی /M را بخوانید و تأیید کنید که یک رشته تاریخ کامل بخش ۷.۹.۴ با پیشوند D: و یک نشانگر آفست است. تأیید کنید که سند منبع در زمان اضافه شدن امضا رمزگذاری نشده بود — و اگر ابزار شما در هر صورت آن را بدون شکایت امضا کرد، با آن سکوت به عنوان یک باگ در ابزار رفتار کنید. هر سه بررسی روی بایت‌های خام در چند میلی‌ثانیه اجرا می‌شوند و در تجربه ما، دلیل رد شدن را بسیار بیشتر از هر دلیل رمزنگاری توضیح می‌دهند

فراخوانی‌های امضا، بازرسی و اعتبارسنجی استفاده شده در اینجا — شامل SignPades، ValidatePades و بررسی‌های انطباق PAdES با محاسبات اصلاح‌شده ByteRange، فرمت‌بندی تاریخ و گیت رمزگذاری آنها — همراه با PDFium Component برای دلفی، C++Builder و لازاروس ارائه می‌شوند