يتحقق 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 أقسام المراجع التبادلية من الأقدم للأحدث مرة واحدة بالضبط، ويعيد تشغيل المدخلات الحرة والمباشرة ومدخلات تدفق الكائنات بحيث تطمس الأقسام الأحدث حالة الأقدم، وهو عكس دلالات «الأسبق يفوز» التي يطبّقها محلل xref النشط
أين ترتكز السلسلة، وما الذي يكسرها؟
أول مراجعة تحمل /AuthCode صالحًا هي المرتكز، وتبلّغ FirstMACRevisionIndex أين تبدأ الحماية؛ وكل ما قبلها غير محمي بحكم البنية، وهذا طبيعي. وكل ما بعدها يجب أن يكون محميًا بـ MAC، فإلحاق تحديث تدريجي مكشوف واحد بملف محمي بـ MAC يفشل برمز pmcfRequiredRevisionMissing مع فهرس المراجعة المخالفة — فالتسامح مع فجوة سيتيح للمهاجم نزع الحماية بمجرد حفظ إضافي. وثلاث ثوابت إضافية تقوم عبر السلسلة، لكل منها رمز فشله
pmcfKDFSaltChanged— يجب أن يبقى/KDFSaltمستقرًا من المرتكز فصاعدًا، لأن ملحًا متغيرًا سيتيح للمزوّر إعادة اشتقاق المفاتيح وفق وسائط يختارها بنفسهpmcfDigestDowngrade— تُقارَن قوة الخلاصة بآخر MAC مُتحقَّق منه لا بالمراجعة السابقة مباشرة، فلا تستطيع سلسلة بدأت بملف Modern عند SHA-384 أن تستمر بهدوء مع SHA-256pmcfPermissionDowngrade— لا يجوز لمراجعة أن تمحو متطلب PDF MAC وثّقته مراجعة أسبق
الأثر الجدير بالاستيعاب هو أن MACs التاريخية تُتحقق منها باستقلال حتى بعد أن تكفّ عن كونها المقطع الختامي النشط. لهذا لا يصمد هجوم تعديل مراجعة قديمة ثم إلحاق MAC جديد من الاستهلال: أحدث MAC سليم في ذاته، وValidatePDFMAC راضية، والسلسلة تحطّ رغم ذلك على المراجعة 1 برمز pmcfRevisionInvalid
ترتيب التوقيع: مفاتيح المقطع الختامي أولًا، وsignatureDigest آخرًا
حين يُربط MAC بتوقيع CMS لا أن يقف وحده، يكفّ ترتيب الكتابة عن كونه مسألة تنسيقية. يتطلب HotPDF أن تُكتب /AuthCode و/KDFSalt وإضافة المطوّر الخاصة بـ ISO 32004 و/SigObjRef في نفس المراجعة قبل حساب /ByteRange الخاص بالتوقيع؛ ألحق أيًا منها بعد ذلك فتقع تلك البايتات خارج النطاق الذي يغطيه التوقيع، منتجًا ملفًا يتحقق توقيعه بينما ربط MAC غير موقَّع. ثم تجري الخلاصتان في الاتجاه الآخر، وهو ما يبدو دائريًا للوهلة الأولى وليس كذلك. فـ signatureDigest الخاص بـ PDF MAC يربط البايتات الخام لقيمة OCTET STRING في SignerInfo.signature بـ CMS — لا كامل DER الخاص بـ CMS، ولا السمات الموقَّعة — لذا يُبنى بعد وجود قيمة التوقيع الخام ويُحقن كسمة غير موقَّعة id-attr-pdfMacData. وبما أن /Contents مستثناة من /ByteRange الخاص بالتوقيع والسمات غير الموقَّعة لا تدخل أبدًا في حساب التوقيع، فإن تسلسل أنتجِ-التوقيعَ ثم ابنِ-MAC ثم غلّف-CMS يُغلق بنظافة بلا حلقة تشفيرية. وتلحق بذلك نتيجتان: حارسة /ByteRange وعنصر /Contents المؤقت يجب أن يبقيا نصًا صريحًا وخارج تدفقات الكائنات حتى في ملف مشفّر، وإلا تعذّر على رتّاب العرض الثابت العثور عليهما؛ وحين تكون خلاصة 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 المباشر من مقطع xref الكلاسيكي النشط حاليًا، وتابع /SigObjRef غير المباشر المدرك للجيل، وأكّد أنه يربط /V حقل التوقيع الوحيد، وأبلِغ فشل خلاصة المستند مستقلًا عن فشل خلاصة التوقيع. إنهما تشخيصان مختلفان، وطمسهما في قيمة منطقية واحدة يهدر المعلومة الوحيدة التي تقول إن كانت محتويات الصفحة أم قيمة التوقيع قد مُست. وإن كنت تفعل أعمال CMS أصلًا، فهذا يجلس إلى جوار مقال التوقيع وفق PAdES ودليل التحقق من التوقيعات في المستندات المحمَّلة
لا تثق بـ /P أبدًا: افك تشفير /Perms الستة عشر بايتًا أولًا
تشير ISO/TS 32004 إلى «هذا المستند يتطلب PDF MAC» عبر بتة الإذن 13، والطريقة الواضحة لقراءتها هي الطريقة الخاطئة، لأن العدد الصحيح /P في قاموس التشفير نص صريح بلا توثيق — أي أحد يستطيع قلب تلك البتة في محرر نصوص وتخفيض المتطلب. تمدّ ISO 32000-2 §7.6 الجواب في مدخل /Perms، ويستخدمه HotPDF: افك تشفير سلسلة /Perms الستة عشر بايتًا بمفتاح تشفير الملف تحت AES-256 CBC، وIV صفري، بلا حشو، ثم افحص كل حقل من النص الصريح قبل أن تصدّق شيئًا. البايتات 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 وفق RFC 3394، ثابتةً تحت THPDFPDFMACDigestAlgorithm متغيرة تمتد من pmdaSHA256 إلى pmdaSHA3_512، لأن الخطأ الطبيعي هو معاملة «ملف SHA3-512» كإذن باستبدال HMAC أيضًا، منتجًا ملفًا لم يعد PDF MAC بأي معنى قابل للتشغيل البيني. ثمت تفصيلة تنفيذ تستحق النقل إن كتبت مدقّقك الخاص: اقرأ OID الخاص بالخلاصة من AuthenticatedData في CMS قبل هشّم نطاق البايتات، لأن تثبيت SHA-256 ثم المسالمة بعد ذلك يحول المرونة إلى ملصق ويتيح لملف عدائي أن يجعلك تمرّر المستند كله قبل أن تكتشف أن الخوارزمية لم تكن مدعومة قط. وكل من CMSAlgorithmProtection وخوارزمية خلاصة AuthenticatedData وmessageDigest في معلومات السلامة وخلاصة نطاق البايتات يجب أن تسمّي خوارزمية واحدة، وأي اختلاف يفشل بإغلاق
var
Options: THPDFPDFMACOptions;
begin
Options := THPDFPDFMACOptions.Compatibility; // SHA-256، تقبل الخيارات الستة
Options := THPDFPDFMACOptions.Modern; // SHA-384، ترفض 256 بت
Options := THPDFPDFMACOptions.HighAssurance; // SHA3-512 فقط، AES-GCM
// الملف المخصص مشروع، لكن الخوارزمية التي يولّد بها
// يجب أن تظهر أيضًا في قائمة السماح للتحقق، وإلا التهيئة
// تُرفض قبل كتابة بايت واحد
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 لكل من Delphi و C++Builder، حيث تحمل صفحة المنتج المرجع الكامل لسجل الخيارات وتعدادات الحالة ومصفوفة التحقق لكل مراجعة