امضای PDF بیشتر به معنای محاسبه بایتها است و دقیقاً همین جا است که معمولاً اشتباه رخ میدهد. رمزنگاری روی کدی اجرا میشود که دو دهه مورد بازبینی قرار گرفته است و آن بخش تقریباً هرگز خراب نمیشود. آنچه در محیط تولید شکست میخورد بسیار سادهتر است: جایگاه رزرو شدهای که برای امضای واقعی بسیار کوچک است، هشی که از بخش اشتباهی از فایل گرفته شده، یا یک "ذخیره" پس از امضا که بیصدا بایتهایی را تغییر میدهد که امضا از پیش آنها را تثبیت کرده بود. بایتها را به درستی در کنار هم قرار دهید تا علامت تیک سبز به صورت خودکار ظاهر شود
HotPDF، امضا کردن در دلفی و C++Builder را در سه سطح پوشش میدهد، و شما با پاسخ به یک سؤال بین آنها انتخاب میکنید: کلید خصوصی کجا قرار دارد؟ یک فایل PFX روی دیسک، تنها به فراخوانی یک تابع نیاز دارد. کلیدی که در HSM یا یک سرویس امضای از راه دور قفل شده است، به توالی رزرو-هش-درج (reserve-hash-insert) نیاز دارد، زیرا هیچ کتابخانهای نمیتواند به درون یک توکن نفوذ کرده و کلید را بیرون بکشد. امضایی که باید با مقررات اروپایی مطابقت داشته باشد، علاوه بر آن به ساختارهای پایه PAdES نیز نیاز دارد. بخشهای زیر این مسیر را دنبال میکنند
چگونه /ByteRange بایتهای امضاشده را تثبیت میکند
یک امضا باید درون فایلی قرار گیرد که آن را امضا میکند، و نمیتواند خودش را امضا کند. PDF این پارادوکس را با باقی گذاشتن یک فضای خالی دور میزند. قبل از امضا، نویسنده یک ورودی /Contents با اندازه ثابت و پر از صفر رزرو میکند و آرایه /ByteRange را برای دو بخش در دو طرف آن ثبت میکند: همه چیز قبل از فضای خالی، و همه چیز بعد از آن. شخص امضاکننده، هش این دو بخش را محاسبه میکند و داده CMS حاصل را به صورت هگزادسیمال درون فضای خالی مینویسد. تله در کلمه ثابت است. شما قبل از آنکه بدانید امضای نهایی چقدر فضا اشغال خواهد کرد، به اندازه این فضای خالی متعهد میشوید، بنابراین مقدار رزرو شده باید یک برآورد بالا با اطمینان کافی باشد. هشت کیلوبایت به راحتی یک امضای مستقل CMS همراه با یک زنجیره گواهی کوتاه را در خود جای میدهد
HotPDF این دو حالت را به دو فراخوانی مجزا تقسیم میکند و اشتباه گرفتن آنها یک خطای اولیه رایج است. AddSignatureField یک فیلد خالی و قابل مشاهده ایجاد میکند تا شخص بعداً در یک نمایشدهنده آن را امضا کند. AddSignedSignatureField فیلد را ایجاد کرده و فضای /Contents را رزرو میکند، که این دقیقاً همان چیزی است که وقتی کد شما (و نه یک انسان) قرار است امضا را تکمیل کند، به آن نیاز دارید. اگر یک فیلد خالی را به یک امضاکننده خارجی تحویل دهید، چیزی برای پر کردن نخواهد داشت
مسیر یکمرحلهای: امضا از طریق فایل PFX
وقتی گواهی و کلید خصوصی آن در یک فایل PFX/PKCS#12 قرار دارند و پردازش شما قادر به خواندن آن است، کل خط لوله به یک تابع کلاس ساده خلاصه میشود:
if THotPDF.SignPDFWithPFX('invoice-unsigned.pdf', 'invoice-signed.pdf',
'company-cert.pfx', 'pfx-password') then
Writeln('Signed: invoice-signed.pdf')
else
raise Exception.Create('PFX signing failed');
وقتی این عملیات با شکست مواجه میشود، به ندرت مشکل از PDF است. مشکل فایل PFX است. HotPDF کانتینرهایی را میخواند که با PBES2 محافظت شدهاند، به این معنی که از استخراج کلید PBKDF2 روی AES-256-CBC استفاده شده است. فایل PFX صادر شده توسط ویزارد گواهی نسخههای قدیمیتر ویندوز، یا OpenSSL قبل از نسخه ۳.۰، معمولاً در پوششهای قدیمی مانند RC2 یا 3DES رمزگذاری شدهاند که قابل پردازش نیستند. راهحل این است که کانتینر را یک بار با محافظت مدرن مجدداً صادر کنید؛ امروزه OpenSSL این کار را به طور پیشفرض انجام میدهد و نیازی به تغییر کد نیست. بنابراین، وقتی امضایی که با گواهی خاصی "در همهجا به درستی کار میکند" در اینجا بلافاصله با شکست مواجه میشود، قبل از اینکه به کد خود شک کنید، نحوه ایجاد فایل PFX را بررسی کنید
مسیر رزرو-هش-درج برای HSMها و توکنها
مسیر یکمرحلهای فرض میکند که پردازش شما میتواند کلید را به عنوان یک فایل بخواند. با گذشت زمان، این امکان به طور فزایندهای از بین میرود. کلید درون یک HSM، یک توکن USB، یا پشت API یک سرویس امضا قرار دارد و هیچ راهی برای دسترسی مستقیم یک کتابخانه به آن وجود ندارد. HotPDF این موضوع را با شکستن عملیات امضا به مراحل سطحبایت مدیریت میکند: نوشتن یک سند به عنوان نگهدارنده (placeholder)، درخواست محدودههای هش از کتابخانه، ارسال ورودی هش به هر چیزی که کلید را در اختیار دارد، و در نهایت وصله کردن CMS بازگشتی به درون فضای خالی
var
Doc: THotPDF;
Fs: TFileStream;
PdfBytes, HashInput, SigHex: AnsiString;
R1Start, R1Len, R2Start, R2Len, CStart, CLen: Integer;
begin
// 1. Write the document with a reserved /Contents hole
Doc := THotPDF.Create(nil);
try
Doc.FileName := 'placeholder.pdf';
Doc.BeginDoc;
Doc.CurrentPage.AddSignedSignatureField('Sig1',
Rect(50, 100, 350, 150), 8192, 'adbe.pkcs7.detached',
'Contract approval', 'Boston, MA', 'legal@example.com');
Doc.EndDoc;
finally
Doc.Free;
end;
// 2. Load the saved bytes; the returned offsets are 0-based
Fs := TFileStream.Create('placeholder.pdf', fmOpenRead);
try
SetLength(PdfBytes, Fs.Size);
Fs.ReadBuffer(PdfBytes[1], Fs.Size);
finally
Fs.Free;
end;
THotPDF.PreparePDFForSigning(PdfBytes, R1Start, R1Len, R2Start, R2Len,
CStart, CLen);
// 3. Hash both spans and sign externally (HSM, token, service)
HashInput := Copy(PdfBytes, R1Start + 1, R1Len) +
Copy(PdfBytes, R2Start + 1, R2Len);
SigHex := SignWithHsm(HashInput); // your integration: returns CMS as hex
// 4. Splice the signature into the reserved hole
THotPDF.InsertSignatureHex(PdfBytes, SigHex);
Fs := TFileStream.Create('signed.pdf', fmCreate);
try
Fs.WriteBuffer(PdfBytes[1], Length(PdfBytes));
finally
Fs.Free;
end;
end;
دو جزئیات در این توالی باعث بروز بیشتر خرابیهای متناوب میشود. اول این است که PreparePDFForSigning روی بایتهای یک فایل نهایی و ذخیرهشده کار میکند. فایل نگهدارنده (placeholder) باید به طور کامل نوشته و ذخیره شود تا آفستها (offsets) معنا پیدا کنند؛ اگر آنها را در جریانی که در حال ایجاد است محاسبه کنید، با بایتهایی که در نهایت هش خواهید کرد همخوانی نخواهند داشت. دومین موضوع، مجدداً اندازه رزرو است. آن ۸۱۹۲ بایتی که درخواست کردید باید CMS نهایی را در خود جای دهد، و امضایی که حامل گواهیهای میانی است، یا امضایی که سرویس آن را با صفات امضاشده تزئین میکند، ممکن است از این حد فراتر رود. InsertSignatureHex فضای خالی را برای باز کردن فضا بزرگ نمیکند. نشانهی چنین خطایی خط لولهای است که با یک گواهی به خوبی امضا میکند اما با گواهی بعدی شکست میخورد؛ راهحل این است که فایل نگهدارنده را با رزروی که بر اساس اندازه واقعی امضای تولید شده توسط امضاکننده اندازهگیری شده است (و نه با حدس زدن)، دوباره ایجاد کنید
پایههای PAdES و مهرهای زمانی که امضا را زنده نگه میدارند
اگر تحت قوانین اروپایی در حال امضا کردن هستید، استانداردی که با آن سر و کار دارید ETSI EN 319 142-1 است که چهار سطح پایه PAdES را تعریف میکند. سطح B-B یک امضای ساده است. B-T یک مهر زمانی قابل اعتماد اضافه میکند که زمان ایجاد امضا را اثبات میکند. B-LT مواد مربوط به اعتبارسنجی یعنی گواهیها و دادههای ابطال را درون سند تعبیه میکند تا سالها بعد هم قابل بررسی باشد. B-LTA مهرهای زمانی دورهای را روی سند اعمال میکند، تا مدارک اعتبارسنجی بیش از عمر الگوریتمهایی که با آن ساخته شدهاند، دوام بیاورند. HotPDF ساختارهای سمت سند را برای هر سطح تولید میکند:
// PAdES baseline signature field (ETSI EN 319 142-1)
Pdf.CurrentPage.AddPAdESSignatureField(
'ApprovalSig', Rect(50, 100, 350, 150), 'B-B',
'Contract approval', 'Boston, MA', 'legal@example.com');
// Document timestamp: larger reservation for the TSA token and chain
Pdf.CurrentPage.AddDocumentTimestampSignature('ArchiveTS', 16384);
رزرو ۱۶۳۸۴-بایتی برای مهر زمانی تعمدی است. یک مرجع صدور مهر زمانی توکنی را برمیگرداند که زنجیره گواهی خاص خودش را نیز به همراه دارد، بنابراین معمولاً به فضای بیشتری نسبت به ۸ کیلوبایتِ امضای معمولی نیاز دارد. این مهرهای زمانی سند همچنین مکانیسمی برای دستیابی به B-LTA هستند: اعمال مجدد مهر زمانی روی یک امضای آرشیو شده هر چند سال یکبار، با استفاده از الگوریتمهایی که همچنان معتبر هستند، چیزی است که باعث میشود سندی که در سال 2026 امضا کردهاید در سال 2040 قابل اعتبارسنجی باقی بماند
یک نکته درباره رشتههای دلیل، موقعیت مکانی و مخاطب که هر دو تابع فیلد میپذیرند: آنها تنها فرادادههایی برای راحتی هستند و نه بیشتر. HotPDF آنها را به عنوان ورودیهای سادهی دیکشنری ذخیره کرده و به صورت متن قابل مشاهده روی امضا ترسیم میکند، اما هیچ اعتبارسنجیای آنها را در برابر چیزی چک نمیکند. آنها را به طور مداوم و منظم از طریق دادههای جریان کاری خود پر کنید، زیرا حسابرسان آنها را مطالعه میکنند، اما هرگز آنها را با شواهد قانونی اشتباه نگیرید. ادعای رمزنگاری واقعی کاملاً در CMS و زنجیره گواهی آن نهفته است و یک سیستم اعتبارسنج متنهای قابل مشاهده را کاملاً نادیده میگیرد
پس از امضا، فایل ممکن است فقط رشد کند
لحظهای که یک امضا وجود دارد، بایتهای داخل محدودههای آن ثابت و دستنخورده باقی میمانند. تنها راه مشروع برای تغییر دادن فایل بعد از آن، اجرای بهروزرسانی افزایشی ISO 32000-1 §7.5.6 است که در آن، اشیاء جدید و تغییریافته پس از بایتهای اصلی اضافه (append) میشوند و بخش جدیدی از ارجاع متقابل را به آنها متصل میکند. با این کار، امضا برای نسخه خود معتبر باقی میماند و نمایشدهنده وضعیت صادقانهای را گزارش میدهد: نسخه امضاشده دستنخورده است، اما سند پس از آن گسترش یافته است. اگر در عوض بخواهید کل فایل را مجدداً به صورت سریالی بنویسید (re-serialize)، محدودههای امضا شده را بازنویسی میکنید و در نتیجه امضا از بین میرود، حتی اگر هیچ تغییری از نظر ظاهری ایجاد نشده باشد. همان مکانیزم نسخه، راهی است که یک سند میتواند حامل چندین امضا باشد: هر امضای جدید در بهروزرسانی افزایشی خودش قرار میگیرد و محدودههای آن همه چیز را پیش از خود از جمله امضاهای قبلی را در بر میگیرد. مکانیزمهای افزودنی و زمان ایمن برای متراکم کردن آنها، در مقالهای درباره جریانهای شیء و بهروزرسانیهای افزایشی توضیح داده شده است
هنگام طراحی، باید به دو محدودیت توجه داشته باشید. حالت خروجی PDF/A در HotPDF، فیلدهای امضا را به طور کامل رد میکند، بنابراین انطباق با بایگانی و داشتن یک امضای تعبیهشده باید به عنوان فایلهای جداگانهای تولید شوند. همچنین، امضا کردن هیچ ارتباطی با محرمانگی ندارد: امضا اثبات میکند که چه کسی سند را تولید کرده و اینکه سند از زمان تولید تغییر نکرده است، اما هر کسی همچنان میتواند آن را بخواند. پنهان کردن محتوا یک وظیفه جداگانه است که توسط رمزنگاری AES-256 و سیاستهای دسترسی اداره میشود
هر چیزی که میسازید، آن را با کدی غیر از کدی که فایل را نوشته است تست کنید. فایل خروجی را در پنل امضای آکروبات باز کنید و سه چیز را تأیید کنید: امضا معتبر است، هویت به ریشه (root) مورد انتظار متصل میشود، و پنل هیچ تغییری را از زمان امضا گزارش نمیدهد. سپس، یک کپی آزمایشی تهیه کنید، تنها یک بایت در محدوده امضاشده را تغییر دهید، و بررسی کنید که پنل اکنون فایل را دستکاریشده تشخیص میدهد. خط لوله امضایی که هرگز فایلهای دستکاریشده را رد نکرده باشد، سیستمی است که در واقع فرآیند اعتبارسنجی آن آزمایش نشده است
هر سه سطح امضا همراه با کامپوننت HotPDF برای دلفی و C++Builder در دسترس است؛ در صفحه محصول میتوانید لینک مرجع کامل APIهای امضا را پیدا کنید