💡 مثال كامل يعمل متاح على GitHub:
pdf-signing-certificate-checks-python

المقدمة

خدمة تقوم بتوقيع ملفات PDF التي تم تحميلها كل ليلة. في أحد الصباحات انتهت صلاحية الشهادة التي تستخدمها، ولم يتغير شيء يبدو: المهمة تُنفّذ، تُكتب الملفات، السجل يبدو طبيعياً. بعد أسابيع يفتح أحدهم أحد تلك المستندات في Acrobat ويظهر شريط تحذير، لأن التوقيع المُصنع بشهادة منتهية الصلاحية ليس توقيعًا أضعف – بل هو توقيع يُبلغ المُصادقون عنه بأنه غير صالح. المستندات التي تبدو مُعتمدة تُصبح أقل قيمة من تلك غير الموقعة، لأن الناس وثقوا بها.

هذا الرفض له اسم. فحص صلاحية الشهادة هو سلوك GroupDocs.Signature للغة Python يرفض التوقيع بمجرد انتهاء فترة صلاحية الشهادة، أو قبل أن تبدأ. ظهر هذا السلوك في الإصدار 26.9 إلى جانب تغييرين لهما الشكل نفسه: أصبح SHA‑256 هو الخلاصة الافتراضية لتوقيعات PDF، وبدأ SignatureSettings.log_level يفلتر بدلاً من أن يُتجاهل بهدوء. كل واحد من هذه التغييرات يأخذ نتيجة كانت تحدث صامتًا ويضعها أمامك.

هذه المقالة تقارن بين تلك الثلاثة ضوابط كما تتصرف من Python عبر .NET – ما الذي يغيّره كل منها في الناتج، ومتى تستخدمه، وأي تفاصيلين من الربط كلفت الناس بعد ظهرًا. كل نتيجة مُقتبسة تأتي من تشغيل العينة على ملف PDF من صفحة واحدة.

لماذا هذا أهم من مجرد ملاحظة إصدار

التغييرات الثلاثة تشترك في خاصية تستحق التسمية: جميعها تُحوّل فشلًا كنت ستكتشفه لاحقًا إلى فشل تكتشفه الآن.

  • الشهادات المنتهية: فشل استدعاء التوقيع حيث يمكن لأحدهم تجديد الشهادة، بدلاً من إنتاج مستندات تفشل التحقق بعد توزيعها.
  • الخلصات الافتراضية: التوقيعات الجديدة تستخدم SHA‑256 دون أن يتذكر أحد طلب ذلك، لذا الخيار الضعيف يتطلب قرارًا بدلاً من الإهمال.
  • مستويات السجل: الخدمة التي تُكوّن التحذيرات فقط الآن تتلقى تحذيرات فقط، ما يجعل التحذيرات قابلة للقراءة، وهذا يعني أنها تُقَرَأ.

هذا الأخير أقل تجميليًا مما يبدو. القيمة الكاملة لتحذير الشهادة المنتهية هي أن أحدهم يراه، والتحذير المدفون بين عشر رسائل تتبع لكل تشغيل توقيع هو تحذير لا يراه أحد.

المتطلبات المسبقة

قبل البدء، تأكد من وجود:

  • Python 3.9 أو أحدث على مفسّر 64‑bit – الحزمة تُضمّ runtime الخاص بـ .NET ولا توجد عجلة 32‑bit.
  • GroupDocs.Signature للغة Python عبر .NET الإصدار 26.10.0، مع رخصة مؤقتة مجانية إذا أردت إزالة حدود التقييم.
  • ملف PDF لتوقيعه، وحزمة cryptography إذا أردت إنشاء شهادات اختبار مؤقتة كما تفعل العينة.

التثبيت

pip install groupdocs-signature-net cryptography

التحكم 1 – الخلاصة المكتوبة داخل التوقيع

hash_algorithm في DigitalSignOptions يختار الخلاصة. الافتراضي منذ الإصدار 26.9 هو SHA‑256، بصيغة adbe.pkcs7.detached التي يتوقعها المُصادقون الحاليون؛ قبل ذلك، كانت التوقيعات الجديدة تستخدم SHA‑1.

with signature.Signature(source_path) as sign:
    options = DigitalSignOptions()
    options.certificate_stream = io.BytesIO(pfx)
    options.password = PASSWORD
    options.hash_algorithm = HashAlgorithm.SHA512
    options.reason = "Approved"

    result = sign.sign(output_path, options)
    return len(result.succeeded)

هناك تفاصيلان تستحقان الإيضاح. الشهادة تُمرّر عبر certificate_stream كـ io.BytesIO بدلاً من مسار ملف، وهذا هو الطريقة التي يصل بها ملف PKCS#12 المبني في الذاكرة إلى المكتبة دون أن يُكتب على القرص – العينة تعتمد على ذلك لتتمكن من عدم شحن أي مفتاح خاص. وHashAlgorithm يقدم القيم AUTO، SHA1، SHA256، SHA384 و SHA512، حيث أن الطابع الزمني، إذا أضفته، يستخدم أي خلاصة استخدمها التوقيع.

عمليًا هذا هو التحكم الذي تلمسه أقل شيء. الافتراضي هو بالفعل الإجابة الصحيحة، SHA384 و SHA512 موجودان عندما تُسمّي سياسة التوقيع إياهما، وSHA1 هو إعداد توافق للمصادقين الذين لا يمكنك تغييرهم.

التحكم 2 – هل تُوقفك شهادة منتهية الصلاحية؟

بدون أي تجاوزات، التوقيع بشهادة انتهت فترة صلاحيتها – أو لم تبدأ بعد – يرفع استثناء GroupDocsSignatureException ولا يكتب شيئًا على الإطلاق.

try:
    sign.sign(output_path, options)
    return True
except signature.GroupDocsSignatureException as error:
    print(f"Rejected: {str(error).splitlines()[0]}")
    return False

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

عندما تحتاج فعلاً إلى التوقيع على أي حال – اختبار على شهادة مؤرشفة، أو دفعة يجب أن تُنفّذ الليلة بينما التجديد جارٍ – يكون التجاوز per call:

settings = signature.SignatureSettings(ConsoleLogger())
settings.log_level = LogLevel.WARNING | LogLevel.ERROR

with signature.Signature(source_path, settings=settings) as sign:
    options.allow_expired = True
    result = sign.sign(output_path, options)

allow_not_yet_valid هو الشكل نفسه لشهادة صادرة لتاريخ لاحق، والعلمتان مستقلتان: السماح بشهادة منتهية لا يسمح بشهادة مبكرة. الشهادة المبكرة عادةً تعني أن ساعة الجهاز خاطئة وليس أن الشهادة غير عادية، وساعة خاطئة تجعل كل توقيع ينتجه الجهاز موضع شك، لذا تحقق من ذلك قبل تجاوز أي شيء.

كلا التجاوزين يُصدران تحذيرًا بدلاً من المرور بصمت، وهذا هو الجزء المتصل بالتحكم الثالث.

التحكم 3 – هل يكتشف أحد؟

SignatureSettings.log_level هو قيمة علمية. العينة توقّع نفس المستند ثلاث مرات، تحت LogLevel.NONE، LogLevel.WARNING | LogLevel.ERROR و LogLevel.ALL، وتعد ما يصل:

logger = CollectingLogger()
settings = signature.SignatureSettings(logger)
settings.log_level = level

with signature.Signature(source_path, settings=settings) as sign:
    options.allow_expired = True
    sign.sign(output_path, options)

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

هناك تفاصيلان في الربط كلفاني بعد ظهرًا، لذا يجدر ذكرهما صراحة. SignatureSettings.logger للقراءة فقط، لذا فإن المُسجّل يُمرّر كوسيط للمنشئ وتعيينه يرفع AttributeError؛ log_level يُضبط بشكل طبيعي بعد ذلك. ومُسجّل مخصص لا يجب أن يرث من groupdocs.signature.logging.ILogger – تلك الفئة الأساسية تُغلف كائنًا أصليًا يحتاج إلى مقبض تملكه المكتبة، لذا الوراثة ترفع TypeError. الربط يمرّر أي كائن عادي يوفر الثلاث طرق التالية:

class StdlibLogger:
    def error(self, message, exception=None):
        logging.getLogger("groupdocs").error(message, exc_info=exception)

    def warning(self, message, exception=None):
        logging.getLogger("groupdocs").warning(message)

    def trace(self, message):
        logging.getLogger("groupdocs").debug(message)

امنح error و warning معاملًا اختياريًا exception. المكتبة لا تمرره دائمًا، ومُسجّل يتطلبه سيكسر عند الرسائل التي لا تتضمنه.

مقارنة الثلاثة: متى تستخدم كلٍ منها

التحكم الأنسب لـ المزايا الرئيسية القيود
hash_algorithm تلبية سياسة تُحدّد خلاصة تعيين واحد؛ نفس حجم الناتج لا فائدة إذا كانت الشهادة نفسها غير موثوقة
فحص الصلاحية والتجاوزات أي توقيع لأشخاص آخرين الفشل يحدث حيث يمكن إصلاحه التجاوز ينتج ملفًا، لكنه ليس موثوقًا
log_level الخدمات التي سجلاتها مشغولة بالفعل أحد عشر رسالة تصبح واحدة يفلتر السجلات فقط، ولا يمنع الاستثناءات

هي ليست بدائل – استدعاء توقيع واحد يستخدم الثلاثة معًا. الترتيب الذي يجب التفكير فيه هو ترتيب العواقب: فحص الصلاحية يقرر ما إذا كان الملف سيُوجد، الخلاصة تحدد ما بداخل الملف، ومستوى السجل يحدد من يعرف.

هل يغيّر مستوى السجل الاستثناءات التي أحصل عليها؟

لا. هو يقرر أي الرسائل تصل إلى المُسجّل ولا شيء أكثر. شهادة منتهية لا تزال تُرفع GroupDocsSignatureException تحت LogLevel.NONE، وallow_expired لا يزال يوقع تحت LogLevel.ALL؛ قيم الإرجاع والاستثناءات متطابقة عبر كل مستوى. ما يتغيّر هو ما إذا كان التحذير الذي يشرح توقيعًا مشكوك فيه يُقرأ أبدًا من قبل شخص.

التحقق انتقل في نفس الاتجاه

جدير بالذكر لأنه النصف الآخر من نفس الإصدار. verify مع DigitalVerifyOptions فارغ الآن يتحقق من كل توقيع رقمي في PDF تشفيرياً، لذا المستند المعدل بعد التوقيع يصبح غير صالح بدلاً من مجرد غير مفسّر:

with signature.Signature(signed_path) as sign:
    result = sign.verify(DigitalVerifyOptions())
    return result.is_valid

سطران، ومن المفيد إضافتهما إلى أي خط أنابيب يوقع ثم يخزن. لاحظ ما لا يَعِد به True: إنه يقول إن التوقيع يطابق المستند، ليس أن المُصدر موثوق. شهادات العينة الموقعة ذاتيًا تُتحقق هنا ولا يزال قارئ PDF يرفضها، ما يجيب على سؤال الثقة بشكل منفصل.

أفضل الممارسات والنصائح

  • اجعل الرفض هو الافتراضي في أي شيء يوقع نيابة عن المستخدمين، وتجاوز per call بدلاً من عالميًا. الاستثناء رخيص؛ دفعة من التوقيعات غير الصالحة ليست كذلك.
  • سجّل نص التحذير، لا مجرد عدّاد. هو يُسمّي الشهادة والتاريخ، وهذا هو الجزء الوحيد الذي يمكن للمشغّل اتخاذ إجراء بشأنه.
  • تحقق من الساعة قبل السماح بشهادة لم تبدأ بعد. الشهادة عادةً صحيحة والآلة عادةً خاطئة، وهذا يؤثر على أكثر من استدعاء توقيع واحد.
  • أبعد رسائل الـ trace من الإنتاج. حوالي عشر رسائل لكل تشغيل توقيع تتراكم بسرعة؛ فعّلها أثناء التشخيص وأطفئها بعد ذلك.
  • تحقق بعد التوقيع في أي خط أنابيب، الآن بعد أن الفحص تشفيري، لذا يُكتشف الإخراج الفاسد قبل أن يجده المستلم.

الخلاصة

ثلاثة ضوابط، استدعاء توقيع واحد، وفكرة التصميم نفسها خلفها جميعًا: النتيجة الخطرة الآن تحتاج إلى قرار، والنتيجة الآمنة لا تحتاج إلى شيء. احتفظ بفحص الصلاحية، عالج allow_expired كاستثناء per call تُسجّله، اترك الخلاصة كما هي إلا إذا قالت سياسة خلاف ذلك، واضبط مستوى السجل بحيث تكون التحذيرات مقروءة.

تشغيل العينة على أحد ملفات PDF الخاصة بك يستغرق دقيقة ويطبع بالضبط ما غيّره كل تحكم – ستة ملفات موقعة، رفض متعمد واحد، وثلاث صفوف من عدّادات الرسائل التي لم تعد متطابقة.

موارد إضافية