HotPDF یک PDF MAC مطابق ISO/TS 32004 را به ازای هر بازنگری اعتبارسنجی میکند، نه به ازای هر فایل. THotPDF.ValidatePDFMACChain هر بهروزرسانی افزایشی را از لنگر زنجیره به جلو طی میکند و هر MAC را روی یک استریم پیشوند فقطخواندنی که در startxref و %%EOF همان بازنگری تمام میشود راستیآزمایی میکند. یک MAC معتبر روی جدیدترین بازنگری درباره بازنگریهای زیرش هیچ چیزی اثبات نمیکند
سناریویی که همه اینها را برانگیخته این است. یک PDF رمزنگاریشده با AES-256 همراه با PDF MAC عرضه میکنید. کسی فایل را در یک ویرایشگر هگز باز میکند، یک بایت داخل اولین بازنگری محافظتشده با MAC را برمیگرداند و بعد یک بازنگری کاملاً تازه با یک MAC کاملاً معتبر از خودش ضمیمه میکند. هر نمایشگری بیاعتراض فایل را باز میکند و یک چکر سادهلوح که بازه بایت فعلی را با MAC در تریلر فعال هش میکند موفقیت گزارش میدهد — چون آن MAC برای بایتهایی که پوشش میدهد واقعاً درست است. خرابی دو بازنگری پایینتر نشسته، در ناحیهای که کسی دوباره بررسیاش نکرده
چرا یک MAC معتبر در سطح بالا اثبات نمیکند که فایل سالم است؟
چون یک PDF MAC یک پیشوند را پوشش میدهد، نه یک سند را. بهروزرسانی افزایشی بخشی درجهیک از خود قالب است: هر ذخیره یک بدنه تازه، یک بخش ارجاع متقابل تازه و یک تریلر تازه ضمیمه میکند، در حالی که بایتهای قدیمی دقیقاً سر جای خودشان میمانند. ISO/TS 32004 سوار بر همان مدل است، پس هر بازنگری دیکشنری /AuthCode خودش را حمل میکند که فایل را همانطور که در آن لحظه بوده احراز میکند، و راستیآزمایی فقط جدیدترین، هر بازنگری قبلی را بدون بررسی میگذارد. HotPDF بنابراین این دو پرسش را به دو فراخوانی عرضه میکند و تفاوتشان تمام نکته این مقاله است. ValidatePDFMAC به «آیا بازنگری فعلی اصیل است» جواب میدهد و یک رکورد THPDFPDFMACValidationInfo پر میکند؛ ValidatePDFMACChain به «آیا هر بازنگری محافظتشده با MAC در این فایل اصیل است» جواب میدهد و THPDFPDFMACChainValidationInfo را با یک آرایه به ازای هر بازنگری بهعلاوه یک دلیل شکست ماشینخوان پر میکند. روی همان فایل دستکاریشده-بعد-MACشده بالا، فراخوانی اول True برمیگرداند و دومی False روی ایندکس بازنگری 1
var
Pdf: THotPDF;
Chain: THPDFPDFMACChainValidationInfo;
I: Integer;
begin
Pdf := THotPDF.Create(nil);
try
if not Pdf.ValidatePDFMACChain('incoming.pdf', 'user', Chain) then
begin
// شکست یکی از pmcfRevisionBoundary یا pmcfNoPDFMAC یا
// pmcfRequiredRevisionMissing یا pmcfRevisionInvalid یا
// pmcfKDFSaltChanged یا pmcfDigestDowngrade یا pmcfPermissionDowngrade است
Writeln('chain rejected: ', Chain.Message);
Writeln('revision ', Chain.FailureRevisionIndex,
' at xref offset ', Chain.FailureXRefOffset);
Exit;
end;
for I := 0 to High(Chain.Revisions) do
Writeln(I, ' len=', Chain.Revisions[I].RevisionLength,
' mac=', Chain.Revisions[I].HasPDFMAC,
' perms=', Chain.Revisions[I].PermissionsAuthenticated);
finally
Pdf.Free;
end;
end;
هر MAC روی استریم پیشوند خودش راستیآزمایی میشود، هرگز روی طول نهایی فایل
گرانترین باگ این حوزه این است که موقع هش دوباره یک بازنگری قدیمی، اندازه نهایی فایل را بهعنوان کران بالا بگیرید؛ کاری که بایتهای انتهایی را به هر دیجست بهجز جدیدترین میچسباند و روی یک فایل سالم دستکاری گزارش میکند. HotPDF بهجای آن برای هر بازنگری یک استریم فقطخواندنی کراندار بازسازی میکند که در مقدار startxref همان بازنگری و به دنبالش %%EOF آن تمام میشود، و فقط همان را هش میکند. پیدا کردن مرز از آنچه به نظر میرسد ظریفتر است: لفظ %%EOF میتواند داخل یک استریم محتوا یا یک رشته ظاهر شود، پس یک نامزد فقط وقتی پذیرفته میشود که startxref بلافاصله قبل از آن به عددی تجزیه شود که برابر آفست ارجاع متقابل بخش در حال اعتبارسنجی است و بین آن دو چیزی جز فضای سفید نباشد. بازنگری بعد دقیقاً یک دنباله پایانخط بعد از مارکر میبلعد — یک CR تنها، یک LF تنها یا یک جفت CRLF — و بیشتر نه. همان قاعده آخر در عمل گاز میگیرد، چون نویسندهای که بین دو بازنگری یک خط خالی اضافه منتشر کند بایتهایی تولید کرده که مال بازنگری بعدی است و بلعیدن همه فضاهای سفید انتهایی در بازنگری قبلی هر دو دیجست را بیسروصدا عوض میکند. شمارش بخشها همان نظم را دنبال میکند: HotPDF بخشهای ارجاع متقابل را از کهنهترین به جدیدترین دقیقاً یک بار طی میکند و مدخلهای free و direct و object-stream را بازپخش میکند تا بخشهای بعدی حالت قبلی را بازنویسی کنند؛ نقطه مقابل معناشناسی اولین-دیده-برندهمیشود که یک تجزیهگر active-xref اعمال میکند
زنجیره کجا لنگر میاندازد و چه چیزی میشکندش؟
اولین بازنگری که /AuthCode معتبر حمل کند لنگر است و FirstMACRevisionIndex گزارش میدهد محافظت از کجا شروع میشود؛ هر چیز قبل از آن طبق ساختار بدون محافظت است که طبیعی است. هر چیز بعد از آن باید با MAC محافظت شود، پس ضمیمه کردن یک بهروزرسانی افزایشی ساده به یک فایل محافظتشده با MAC با pmcfRequiredRevisionMissing و ایندکس بازنگری متخلف شکست میخورد — تحمل یک شکاف به مهاجم اجازه میدهد محافظت را صرفاً با یک ذخیره دیگر براندازد. سه ناوردا دیگر هم روی کل زنجیره برقرارند، هرکدام با کد شکست خودش
pmcfKDFSaltChanged—/KDFSaltباید از لنگر به بعد پایدار بماند، چون نمکی که بچرخد به جعلکننده اجازه میدهد کلیدها را زیر پارامترهای انتخاب خودش دوباره مشتق کندpmcfDigestDowngrade— قدرت دیجست با آخرین MAC راستیآزماییشده مقایسه میشود نه با بازنگری بلافاصله قبلی، پس زنجیرهای که زیر پروفایل Modern در SHA-384 شروع شده نمیتواند بیسروصدا با SHA-256 ادامه یابدpmcfPermissionDowngrade— یک بازنگری نمیتواند الزام PDF MAC ای که یک بازنگری قبلی احرازش کرده را پاک کند
نتیجهای که ارزش درونیسازی دارد این است که MACهای تاریخی مستقلاً راستیآزمایی میشوند حتی وقتی دیگر تریلر فعال نیستند. برای همین حمله «ویرایش یک بازنگری قدیمی و بعد ضمیمه یک MAC تازه» از مقدمه مقاله دوام نمیآورد: جدیدترین MAC خودش درست درمیآید، ValidatePDFMAC راضی است، و زنجیره همچنان با pmcfRevisionInvalid روی بازنگری 1 فرود میآید
ترتیب امضا: اول کلیدهای تریلر، آخر signatureDigest
وقتی MAC به یک امضای CMS چسبانده میشود نه آنکه تنها بایستد، ترتیب نوشتن از یک پرسش سلیقهای خارج میشود. HotPDF الزام دارد /AuthCode و /KDFSalt و افزونه توسعهدهنده ISO 32004 و /SigObjRef پیش از محاسبه /ByteRange امضا در همان بازنگری نوشته شوند؛ اگر هرکدام را بعداً اضافه کنید آن بایتها بیرون از بازهای که امضا پوشش میدهد میافتند و فایلی تولید میشود که امضایش راستیآزمایی میشود در حالی که پیوند MAC امضا نشده است. دو دیجست بعد در جهت دیگری میروند که در نگاه اول حلقوی به نظر میرسد و نیست. signatureDigest در PDF MAC به اکتتهای خام محتوای OCTET STRING در SignerInfo.signature CMS میچسبد — نه کل CMS DER و نه ویژگیهای امضاشده — پس بعد از آنکه مقدار خام امضا وجود داشت ساخته میشود و بهشکل یک ویژگی بدون امضای id-attr-pdfMacData تزریق میشود. چون /Contents از ByteRange امضا مستثناست و ویژگیهای بدون امضا هرگز وارد محاسبه امضا نمیشوند، توالی تولید-امضا و ساخت-MAC و پیچیدن-CMS تمیز بسته میشود بدون هیچ حلقه رمزنگارانه. دو نتیجه فرعی هم دنبال میشود: نگهبان /ByteRange و جاینگهدار /Contents باید plaintext بمانند و بیرون از استریمهای آبجکت، حتی در فایل رمزنگاریشده، وگرنه پچر عرض ثابت نمیتواند پیدایشان کند؛ و وقتی دیجست MAC هم SHA-256 است دیجست امضا مستقیماً بازیافت میشود، وگرنه هر دو کانتکست دیجست در یک گذر روی استریم خروجی بهروز میشوند
var
Pdf: THotPDF;
Options: THPDFPDFMACOptions;
Info: THPDFPDFMACValidationInfo;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.AutoLaunch := False;
Pdf.FileName := 'unsigned.pdf';
Pdf.ActivateProtection := True;
Pdf.CryptKeyLength := aesgcm;
Pdf.OwnerPassword := 'owner';
Pdf.UserPassword := 'user';
Pdf.ProtectOptions := [prPrint, prExtractContent];
Pdf.BeginDoc;
Pdf.CurrentPage.SetFont('Arial', [], 12);
Pdf.CurrentPage.AddSignedSignatureField('Approval',
Rect(72, 120, 280, 160), 16384);
Pdf.EndDoc;
Options := THPDFPDFMACOptions.Modern; // دیجست سند SHA-384
if THotPDF.SignPDFWithPFXAndAttachedPDFMAC('unsigned.pdf',
'signed.pdf', 'signer.pfx', 'pfx-secret', 'user', Options) then
if Pdf.ValidatePDFMAC('signed.pdf', 'user', Options, Info) then
begin
// Location برابر pmlAttachedToSignature است و دو دیجست
// جداگانه گزارش میشوند
Writeln('signature object : ', Info.SignatureObjectNumber);
Writeln('signature digest : ', Info.SignatureDigestMatched);
Writeln('full file coverage: ', Info.FullFileCoverage);
Writeln('perms authentic : ', Info.PermissionsAuthenticated);
end;
finally
Pdf.Free;
end;
end;
اعتبارسنجی همان مسیر را از سر دیگر بازپیمایی میکند: /AuthCode مستقیم را از تریلر کلاسیک ارجاع متقابلِ در حال حاضر فعال میخواند، /SigObjRef غیرمستقیم آگاه از generation را دنبال میکند، تأیید میکند که به /V همان یک فیلد امضا میچسبد و شکست دیجست سند را جدا از شکست دیجست امضا گزارش میکند. این دو تشخیصهای متفاوتیاند و فروپاشیدنشان در یک بولی تنها اطلاعاتی را دور میریزد که میگوید محتوای صفحه دست خورده یا مقدار امضا. اگر از قبل کار CMS انجام میدهید، این موضوع در کنار مقاله امضای PAdES و راهنمای راستیآزمایی امضاها در اسناد لودشده مینشیند
هرگز به /P اعتماد نکنید: اول رشته 16 بایتی /Perms را از رمز درآورید
ISO/TS 32004 پیام «این سند به یک PDF MAC نیاز دارد» را از طریق بیت مجوز 13 میفرستد و راه واضح خواندنش راه غلط است، چون عدد صحیح /P در دیکشنری رمزنگاری plaintext و بدون احراز است — هر کسی میتواند آن بیت را در یک ویرایشگر متن برگرداند و الزام را تنزل دهد. ISO 32000-2 §7.6 جواب را در مدخل /Perms میدهد و HotPDF از همان استفاده میکند: رشته 16 بایتی /Perms را با کلید رمزنگاری فایل زیر AES-256 CBC با IV صفر و بدون padding از رمز درآورید، بعد قبل از باور کردن هر چیزی تکتک فیلدهای plaintext را چک کنید. بایتهای 1 تا 4 مقدار مجوز را به ترتیب little-endian نگه میدارند و باید دقیقاً با عدد صحیح /P برابر باشند؛ بایتهای 5 تا 8 برابر 0xFF؛ بایت 9 فلگ رمزنگاری فراداده T یا F است؛ بایتهای 10 تا 12 مارکر لفظی adb هستند. فقط وقتی همه آنها برقرار است PermissionsAuthenticated میشود True و بیت 13 خوانده میشود — و قطبیتش را در نظر بگیرید، چون الزام MAC وقتی مطرح میشود که بیت 0x1000 خاموش باشد. ناهمخوانی بین /P و مجوزهای از رمز درآمده یک هشدار برای لاگ کردن و رد شدن نیست؛ یک مجموعه مجوز جعلی است و پاسخ درست شکست امن است
چابکی الگوریتم در دیجست متوقف میشود
ISO/TS 32004 اجازه میدهد دیجست سند را انتخاب کنید و فقط دیجست سند را. HotPDF HMAC-SHA-256 را برای احراز و HKDF-SHA-256 مطابق RFC 5869 برای مشتق کلید و AES-256 key wrap مطابق RFC 3394 را ثابت نگه میدارد زیر یک متغیر THPDFPDFMACDigestAlgorithm که از pmdaSHA256 تا pmdaSHA3_512 را میپوشاند، چون اشتباه طبیعی این است که «پروفایل SHA3-512» را جواز تعویض HMAC هم بگیرید؛ کاری که فایلی تولید میکند که دیگر به هیچ معنای بینعملیاتی یک PDF MAC نیست. یک جزئیات پیادهسازی ارزش کپی کردن دارد اگر وریفایر خودتان را مینویسید: OID دیجست را پیش از هش کردن بازه بایت از AuthenticatedData در CMS بخوانید، چون هاردکد کردن SHA-256 و تطبیق بعدی، چابکی را به یک برچسب تبدیل میکند و به یک فایل خصمانه اجازه میدهد شما را وادار کند کل سند را استریم کنید و بعد کشف کنید الگوریتم اصلاً پشتیبانی نمیشده. CMSAlgorithmProtection و الگوریتم دیجست AuthenticatedData و messageDigest در integrity-info و دیجست بازه بایت همه باید یک الگوریتم را نام ببرند و هر ناهمخوانی شکست امن است
var
Options: THPDFPDFMACOptions;
begin
Options := THPDFPDFMACOptions.Compatibility; // SHA-256، هر شش مورد را میپذیرد
Options := THPDFPDFMACOptions.Modern; // SHA-384، 256 بیتی را رد میکند
Options := THPDFPDFMACOptions.HighAssurance; // فقط SHA3-512، با AES-GCM
// یک پروفایل سفارشی قانونی است، اما الگوریتمی که با آن تولید
// میکند باید در allowlist اعتبارسنجی هم بیاید، وگرنه پیکربندی
// پیش از نوشتن حتی یک بایت رد میشود
Options.Profile := pmppCustom;
Options.DigestAlgorithm := pmdaSHA512;
Options.AllowedDigestAlgorithms := [pmdaSHA512, pmdaSHA3_512];
Options.RequireAESGCM := True;
end;
یک PDF MAC چه چیزی را اثبات میکند و چه چیزی را نه
یک زنجیره PDF MAC راستیآزماییشده اثبات میکند که هر بازنگری محافظتشده بایتبهبایت با آنچه کسی که کلید رمزنگاری فایل را داشته نوشته یکی است، که هیچ بازنگری محافظتشدهای حذف یا جابهجا نشده و که هیچ بازنگری بدون محافظتی بعد از لنگر ضمیمه نشده — دقیقاً همان کلاس حملهای که رمزنگاری AES-256 ساده باز میگذارد، چون محرمانگی چیزی درباره یکپارچگی نمیگوید و یک PDF رمزنگاریشده با بازنگری دوختهشده به همان آسانی نسخه سالمش از رمز درمیآید. آنچه اثبات نمیکند نویسندگی است. کلید MAC از کلید رمزنگاری فایل مشتق میشود، پس هر کسی که بتواند سند را باز کند میتواند روی نسخهای دستکاریشده یک MAC معتبر تولید کند، همه گیرندگان مشروع مشمولش هستند؛ این یک پریمیتیو متقارن است و پریمیتیوهای متقارن انتساب نمیدهند. اگر لازم است بدانید چه کسی چیزی را عوض کرده به یک امضای دیجیتال با گواهی پشتش نیاز دارید و PDF MAC بعد آن را مکمل میکند با محافظت از ساختار افزایشیای که امضا بهتنهایی پوشش نمیدهد. آنها را لایه ببینید و بگذارید دو رأی مستقلاً گزارش شوند تا در یک آیکن وضعیت فرو نریزند
نقطههای ورود PDF MAC توصیفشده در این مقاله — AddStandalonePDFMAC، SignPDFWithPFXAndAttachedPDFMAC، ValidatePDFMAC و ValidatePDFMACChain — همراه HotPDF Delphi Component استاندارد برای Delphi و C++Builder عرضه میشوند؛ صفحه محصول آنجا مرجع کامل رکورد آپشنها، شمارشگرهای وضعیت و آرایه اعتبارسنجی به ازای هر بازنگری را دارد