مقال تقني

التحقق من تواقيع PDF بـ OpenSSL في PDFium VCL

يعامل PDFium VCL التحقق من CMS بوصفه خلفية قابلة للاستبدال خلف واجهة IPdfCmsVerifier، فيستطيع مدقق PAdES أن يجري على Windows عبر CryptoAPI، وعلى macOS عبر Keychain، وفي أي مكان يوجد فيه OpenSSL عبر ConfigureSslCmsVerifier. والواجهة صغيرة. وثلاثة سلوكيات في OpenSSL تحتها تنتج أجوبة خاطئة واثقة إن نفذتها بسذاجة

والدافع واضح بما يكفي ما إن يغادر تطبيق Delphi نظام Windows. فالتحقق من التواقيع واحدة من المناطق القليلة التي لا تكون فيها رصة تشفير المنصة تفصيلة تنفيذ: فهي تقرر أي الشهادات موثوقة، وأي الخوارزميات موجودة، وماذا يعني الإبطال. ثبّت واحدة كتابعةً فلا ينتقل الكود، وجردِها جردًا سيئًا فستبلّغ كل منصة جوابًا بشكل مختلف لا يستطيع المستدعي مقارنته

ما يجب أن تحمله التجريد فعلًا

شكلان للتحقق وثلاثة أحكام مستقلة. فتوقيع PDF منفصل: المحتوى الموقع هو مدى البايتين على جانبي ثقب /Contents، فتأخذ VerifyDetached مقطعين لا مخزنًا واحدًا. ورمز الطابع الزمني مرفق، يحمل محتواه هو، فتأخذ VerifyAttached قيمة DER فقط

وينقسم الناتج إلى ثلاث حالات لأنها تجيب عن ثلاثة أسئلة مختلفة ويمكن أن تختلف. فـ SignatureStatus تقول هل وقّعت البايتات بالمفتاح في شهادة الموقّع. وTrustStatus تقول هل تتسلسل تلك الشهادة إلى شيء تثق به. وRevocationStatus تقول هل كانت الشهادة ما زالت صالحة في الوقت الموافق. فوثيقة بتوقيع مثالي رياضيًا من شهادة لم تسمع بها قط صالحة وغير موثوقة ومجهولة، وطي ذلك في قيمة منطقية واحدة هو كيف ينتهي المدققون إلى الكذب على المستخدمين

uses
  FPdfCrypto, FPdfCryptoSsl;

var
  Options: TPdfCmsVerifyOptions;
begin
  if not SslAvailable then
    raise Exception.Create('libcrypto not usable: ' + SslMissingSymbols);

  ConfigureSslTrustAnchors(LoadCorporateRoots);   // DER، قد يكون فارغًا
  ConfigureSslCrls(LoadFreshCrls);                // DER، قد يكون فارغًا
  ConfigureSslCmsVerifier;                        // ينصب الخلفية

  Writeln('backend  : ', PadesCmsVerificationBackendName);
  Writeln('library  : ', SslLibraryPath, ' ', SslLibraryVersion);
  Writeln('ABI      : ', SslAbiLayout);           // ulong=<n> long=<n>

  Options := TPdfCmsVerifyOptions.Default;
  Options.CheckRevocation := True;
  Options.CollectChainCertificates := True;
end;

وSslAbiLayout تبدو فضولًا وليست كذلك. فكل رمز خطأ في OpenSSL وكل علم مخزن يعبر الحد بوصفه unsigned long في C، وهو أربع بايتات على Windows وثمانية على Linux وmacOS. أعلنها نوعًا ثابتًا 32-بت وسيعمل الكود على Windows، ثم سيقرأ نصف قيمة بصمت على LP64. وتبليغ العروض المفترضة بوصفها سلسلة تستطيع التحقق منها في اختبار يحوّل صنفًا كاملًا من انحرافات ABI المنصات إلى فحص سطر واحد. وكل من عمل على المشكلة نفسها مع CK_ULONG في ربط PKCS#11 سيتعرف عليها فورًا؛ وتلك القصة في حزم بنى PKCS#11 وعرض CK_ULONG

لماذا ترى تمريرة التحقق الثانية محتوى فارغًا

لأن CMS_verify تقرأ BIO المحتوى المنفصل إلى نهاية الملف، وBIO قُرئ لا يُعاد لفُه لك. فالتحقق في تمريرتين تصميم معقول، أولًا التوقيع التشفيري وحده مع كبت تقييم السلسلة، ثم التقييم الكامل، وهو يخفق بطريقة خادعة على نحو غير عادي إذا شاركت التمريرتان BIO واحدًا

تحصل التمريرة الثانية على صفر بايت من المحتوى. وفي الوضع المنفصل ذلك ليس خطأ، لأن مخزن محتوى فارغ مدخل مشروع. فالملخص ببساطة لا يطابق، ويظهر الإخفاق بوصفه إخفاق بناء سلسلة لا إخفاق محتوى، فيرسلك ذلك تفحص الشهادات ومخازن الثقة بينما المشكلة الفعلية موضع تدفق. وأعد بناء BIO الذاكرة بـ BIO_new_mem_buf لكل تمريرة. تكلفة ذلك تخصيص واحد ويزيل الاحتمال كليًا

ما يكبته علم no-verify وما لا يكبته

CMS_NO_SIGNER_CERT_VERIFY تكبت تقييم السلسلة لا البحث عن شهادة الموقّع. فداخليًا تحل OpenSSL وتلحق شهادات الموقّع قبل أن ترجع إلى العلم، فبعد تمريرة أولى تحمل ذلك العلم يكون الموقّع متاحًا أصلًا ويمكن قراءة معرفات خوارزميته فورًا. ولا حاجة إلى جري تحقق كامل ثانٍ فقط للحصول على شهادة الموقّع، وهو ما يغريلك إليه اسم العلم

وقاعدة ملكية واحدة ترافق ذلك. فمرجع الموقّع يعود إلى بنية CMS ولا يجوز تحريره باستقلال. وهو صالح طالما البنية صالحة، وتحريره ينتج تلفًا يظهر عرضه في مكان آخر كليًا، عادة أثناء تنظيف كائن لا صلة له

لماذا يرفض تشغيل فحص CRL كل توقيع

لأن OpenSSL تفحص قوائم CRL فقط مقابل ما يحمله المخزن أصلًا ولا تجلب شيئًا بنفسها. فهي لا تتبع نقاط توزيع CRL ولا تتحدث OCSP. اضبط X509_V_FLAG_CRL_CHECK على مخزن لا يحوي CRLs وستخفق كل سلسلة بعدم القدرة على الحصول على CRL لشهادة. والناتج يبدو كفحص إبطال يعمل ويجد مشكلات. وهو فحص إبطال لا يجري أصلًا

لذا تضبط الخلفية العلم فقط حين يكون ConfigureSslCrls قد زود فعلًا بقائمة CRL واحدة على الأقل. ودون واحدة، تعود RevocationStatus بوصفها pcvsUnsupported، وهي تصريح أمين بأن السؤال لم يُجب. وللسبب نفسه ليس لـ OnlineRetrieval أثر على هذه الخلفية ولا تُصدر نقطة تفتيش pcvstOnlineRetrieval: فلا مسار جلب يُبلَّغ عن تقدمه

مخطط مدقق CMS عبر OpenSSL في PDFium VCL لثلاثة فخاخ: BIO محتوى مشترك قُرئ إلى نهاية الملف يترك تمريرة التحقق الثانية بصفر بايت، وCMS_NO_SIGNER_CERT_VERIFY تكبت تقييم السلسلة لا البحث عن الموقّع، وفحص CRL على مخزن فارغ يرفض كل سلسلة دون أن يجري الإبطال أبدًا
كل فخ ينتج حكمًا خاطئًا واثقًا: موضع تدفق يتخفى بوصفه إخفاق ثقة، وعلم no-verify يكبت أقل مما يوحي اسمه، وإبطال لم يجري قط يبدو بوصفه إبطالًا وجد مشكلات

وهذا موقف تصميم يستحق الدفاع عنه عمومًا. فالمدقق الذي لا يستطيع فحص الإبطال ينبغي أن يقول ذلك. وتبليغ شهادة لم تُفحص بوصفها غير ملغاة هي الطريقة الأكثر شيوعًا منفردة التي تخدع بها أدوات التحقق من التواقيع مستخدميها، وهو بالضبط صنف الالتباس الذي تستكشفه مقالة لماذا يرفض المدققون تواقيع PAdES

// نقاط التفتيش تتيح لواجهة أن تعرض أي مرحلة تجري، وتخبرك أي
// المراحل تجريها خلفية فعلًا
type
  TSignatureProbe = class
    procedure Checkpoint(Stage: TPdfCmsVerifyStage);
  end;

procedure TSignatureProbe.Checkpoint(Stage: TPdfCmsVerifyStage);
begin
  case Stage of
    pcvstCryptographicSignature: Status('checking the signature');
    pcvstChainBuild:             Status('building the certificate chain');
    pcvstOnlineRetrieval:        Status('fetching validation data');
    pcvstRevocationCheck:        Status('checking revocation');
  end;
end;

// اقرأ الأحكام الثلاثة منفصلة؛ يجوز لها أن تختلف
if Result.SignatureStatus = pcvsValid then
  case Result.TrustStatus of
    pcvsValid:         Report('signed and trusted');
    pcvsInvalid:       Report('signed, chain rejected');
    pcvsUnsupported,
    pcvsIndeterminate: Report('signed, trust not established');
  end;
if Result.RevocationStatus = pcvsUnsupported then
  Report('revocation was not checked on this backend');

الربط بمكتبة لا تستطيع تثبيتها

أعادت OpenSSL تسمية مُتّصلات رصتها بين 1.0 و1.1، فالدالة المنطقية نفسها لها اسمان محتملان للتصدير بحسب البناء الذي يصادف أن المضيف يملكه. ويحل الربط الاسم الأحدث أولًا ويرتد إلى الأقدم، ولا يسجل رمزًا مفقودًا إلا إذا لم يحل أي منهما. وذلك هو الشكل الصائب لأي ربط ديناميكي مقابل مكتبة لا تشحنها أنت: فضّل الأسماء الحالية، وتسامح التاريخية منها، وابلّغ عن الغياب الحقيقي وحده

وSslMissingSymbols هي ما يحوّل تحميلًا فاشلًا إلى حدث قابل للتشخيص. فناتج غير فارغ على مضيف يحمل libcrypto مثبتة بوضوح معناه أن الإصدار المثبت أقدم من الواجهة التي يستهدفها هذا البناء، وهو حديث دعم مختلف كليًا عن مكتبة مفقودة. وConfigureSslLibraryPath تغطي الحالة الشائعة الأخرى: مضيف ببناءات OpenSSL عدة يكون الذي على مسار البحث الافتراضي غير الذي تريده

اختيار خلفية لكل منصة

والترتيب العملي أن تختار عند البدء وتسجل أي واحدة أجابت. فعلى Windows، تتكامل الخلفية المنصوية مع مخازن الشهادات التي تديرها مؤسسة أصلًا، وهذا عادة ما تريده. وعلى macOS تناسب خلفية Keychain الاستدلال نفسه وهي مشروحة في التحقق من التواقيع بـ SecTrust على macOS. وOpenSSL هي الخيار المحمول، وهي أيضًا الخيار الصائب حين تحتاج سياسة تحقق مطابقة عبر المنصات لا سياسة تتبع مخزن ثقة كل منصة

مخطط PDFium VCL لتجريد IPdfCmsVerifier الحامل VerifyDetached على مدى البايتين حول ثقب Contents وVerifyAttached لرموز الطوابع الزمنية، والأحكام الثلاثة المستقلة SignatureStatus وTrustStatus وRevocationStatus، والخلفيات لكل منصة تختار عند البدء عبر CryptoAPI أو SecTrust أو ConfigureSslCmsVerifier
تحمل الواجهة شكلين للتحقق وثلاثة أحكام لأنها تجيب عن أسئلة مختلفة ويمكن أن تختلف، وتُسجل الخلفية المنصبة بجوار كل حكم حتى يمكن إعادة إنتاج النتائج المخزنة

أيًا ما تنصب، سجّل PadesCmsVerificationBackendName بجوار كل حكم تحفظه. فنتيجة تحقق مخزنة بلا الخلفية التي أنتجتها لا يمكن إعادة إنتاجها لاحقًا، لأن قيم الحالات الثلاث تعني أشياء مختلفة بخفة بحسب أي رصة أجابت. وطبقة فحص التواقيع فوق كل هذا، شاملةً كيفية تبليغ مستويات PAdES، مغطاة في فحص التواقيع الرقمية لـ PDF ومستويات PAdES

وكل ذلك يرد شيفرة مصدرية مع مكوّن PDFium Delphi، وهذا يهم هنا أكثر من العادة: فلمدقق تواقيع، القدرة على قراءة أي أعلام تضبطها خلفية بالضبط وأي فحوص تتخطاها ليس رفاهية، بل هو الطريقة الوحيدة لمعرفة ما تدعيه علامة الصح الخضراء في تطبيقك فعلًا