💡 مثال كامل يعمل متاح على GitHub:
digital-signing-certificate-validity-dotnet

مشكلة الامتثال التي لا يراها أحد حتى يأتي المدقق

تعمل خدمة التوقيع لمدة ثلاث سنوات دون أي خطأ. تُرسل المستندات، يقبلها المستلمون، ولا يشير شيء في السجلات إلى وجود مشكلة. ثم يُعلم مدقق الطرف المقابل دفعة بأنها غير صالحة، وتظهر التحقيقات سببين: تم كتابة التوقيعات باستخدام SHA-1، وللأربعة أشهر الأخيرة كان الشهادة منتهية الصلاحية.

كلا الفشلين كان صامتًا في لحظة التوقيع. هذا هو ما تغيره GroupDocs.Signature 26.9.

إن فرض صلاحية الشهادة هو السلوك الافتراضي الجديد لتوقيع .NET الرقمي: تُرفض الشهادة خارج نافذة صلاحيتها بدلاً من استخدامها. يأتي ذلك مع رفيقين — SHA-256 كخلاصة PDF الافتراضية، وLogLevel الذي يفلتر أخيرًا — ومعًا ينقلون ثلاث فئات من الفشل من المستلم إلى المُرسل، حيث لا يزال بإمكانهم إصلاحها.

لماذا النجاح الصامت هو النتيجة المكلفة

التوقيع غير عادي لأن الطرف الذي يرتكب الخطأ ليس هو الطرف الذي يكتشفه. فشل فاتورة مشوهة في نظامك؛ فشل توقيع غير صالح في نظام شخص آخر، بعد أسابيع، دون أي تشخيص يمكنك قراءته.

هذا الاختلاف هو السبب في أن “واجهة برمجة التطبيقات أعادت نجاحًا” ليست ضمانًا مفيدًا هنا. الإعدادات الافتراضية القديمة كانت تُحسّن عدم إيقاف المتصل، وتكبدت التكلفة على المستلم، وفي النهاية على من اضطر إلى إعادة التوقيع وإعادة إرسال مئات المستندات.

التغيير 1: رفض الشهادات المنتهية الصلاحية

التغيير الرئيسي. الآن Sign يرمي GroupDocsSignatureException عندما تكون صلاحية الشهادة قد انتهت أو لم تبدأ بعد، ولا يُكتب شيء إلى القرص.

try
{
    signature.Sign(outputPath, options);
    return true;
}
catch (GroupDocsSignatureException ex)
{
    Console.WriteLine($"   Rejected: {ex.Message}");
    return false;
}

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

عندما تحتاج حقًا إلى السلوك القديم، هناك خاصية واحدة:

var options = new DigitalSignOptions(certificate)
{
    Password = certificatePassword,
    AllowExpired = true
};

يُوقع المستند وتُرسل تحذير إلى السجل. لا يزال المدققون يرفضون النتيجة، لأن AllowExpired يتحكم فيما تسمح به المكتبة وليس فيما تستحقه الشهادة. العلم المرافق AllowNotYetValid يغطي الطرف الآخر من النافذة وهو مستقل عمدًا: السماح بشهادة منتهية لا يعني السماح بهدوء بشهادة مؤرخة في المستقبل.

التغيير 2: SHA-256 كإعداد افتراضي

الآن تُكتب توقيعات PDF الرقمية باستخدام SHA-256 في تنسيق adbe.pkcs7.detached الذي يتوقعه المدققون الحاليون. الإصدارات السابقة كانت تستخدم SHA-1.

var options = new DigitalSignOptions(certificate)
{
    Password = certificatePassword,
    HashAlgorithm = HashAlgorithm.Sha256,
    Reason = "Approved",
    Location = "Head office"
};

تعيين الخاصية صراحةً يصبح ضروريًا فقط للانتقال إلى خوارزمية أقوى — Sha384 أو Sha512 عندما تتطلب السياسة ذلك — أو للبقاء على Sha1 إذا كان المدقق لا يستطيع التعامل مع غير ذلك. الطابع الزمني المضاف إلى التوقيع يستخدم نفس الخلاصة.

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

التغيير 3: LogLevel يفلتر فعليًا

SignatureSettings كان يقبل مسجلًا (logger) منذ زمن طويل. قبل 26.9 كان المستوى يُتجاهل، لذا كانت كل رسالة تصل بغض النظر، وغالبًا ما تُطفئ الخدمات التسجيل بدلاً من الغرق في الآثار.

العينة تجعل الفرق قابلًا للقياس عبر توقيع نفس المستند ثلاث مرات باستخدام مسجل عدّ:

var levels = new Dictionary<string, LogLevel>
{
    ["None"] = LogLevel.None,
    ["Warning | Error"] = LogLevel.Warning | LogLevel.Error,
    ["All"] = LogLevel.All
};

None ينتج صفر رسائل، Warning | Error يحتفظ بالتحذير الوحيد الناتج عن الشهادة المنتهية المسموح بها، وAll يضيف أثرًا لكل خطوة. مسجل العد نفسه هو نقطة التكامل مع مجموعة أدواتك الخاصة:

public void Warning(string message)
{
    Warnings++;
    WarningMessages.Add(message);
}

نفّذ هذه الطرق الثلاثة باستخدام Serilog أو NLog أو Application Insights وستصل تشخيصات المكتبة إلى أي مكان تُسجِّل فيه بقية خدمتك.

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

لا، ومن المفيد أن تكون صريحًا لأن الاثنين يبدوان مرتبطين. LogLevel يفلتر ما يصل إلى ILogger. تُرمى الاستثناءات إلى شفرتك بغض النظر: شهادة منتهية بدون AllowExpired لا تزال تُرمى حتى عند LogLevel.None، وسلوك كتلة catch يبقى هو نفسه. التشخيص وتدفق التحكم قناتان منفصلتان، وهذا ما يجعل تشغيل الإنتاج عند Warning | Error آمنًا.

الرفض أرخص مما يبدو

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

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

ما الذي يجب فعله قبل الترقية

ثلاث فحوصات، بترتيب احتمال حدوثها.

  1. راقب انتهاء صلاحية الشهادات عبر كل مسار توقيع، بما في ذلك تلك التي تُنفّذ شهريًا أو ربع سنويًا — فهذه هي الأماكن التي تُخفي فيها الشهادة المنتهية أطول فترة.
  2. ابحث عن HashAlgorithm: إذا لم يُحدد شيء، سيتغيّر الخلاص من SHA-1 إلى SHA-256 عند الترقية، وهذا تحسين يجب ذكره في ملاحظات الإصدار.
  3. قرّر مستوى السجل عمدًا. الإعداد الصادق الافتراضي لخدمة هو Warning | Error؛ All يُستخدم لإعادة إنتاج مشكلة محددة، وNone يعني التخلي عن الإشارة الوحيدة التي تخبرك بأن توقيعًا تم تحت استثناء.

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

من السهل أن يغيب عن البال، لأن لا شيء في الكود المستدعي يحتاج إلى تغيير. DigitalVerifyOptions بدون معايير كان تقريبًا لا يفعل شيئًا: كان يقارن المعايير التي أعطيته إياها، وعند عدم إعطاء أي معيار، لم يكن له ما يقوله. من الإصدار 26.9، نفس الاستدعاء يُجري فحصًا تشفيريًا كاملًا لكل توقيع PDF رقمي.

بالنسبة لخدمة تتحقق من المستندات الواردة، هذا ترقية صامتة من “هناك توقيع هنا” إلى “هذا التوقيع يطابق هذا المحتوى”. من المفيد معرفة ذلك قبل أن ترى مستندًا يبدأ بالفشل في التحقق بعد أن كان ينجح الشهر الماضي: من المحتمل أن المستند قد تم تعديلَه، والفحص الأقدم ببساطة لم يلقِ نظرة.

الشهادات في العينة

تفصيل يستحق النسخ بدلاً من الكود: العينة لا تُرفق مفتاحًا خاصًا. TestCertificates.cs يُنشئ ثلاث شهادات PFX موقعة ذاتيًا في الذاكرة وقت التشغيل — صالحة، منتهية العام الماضي، صالحة من العام القادم — لذا يعمل العرض بغض النظر عن تاريخ اليوم ولا يحتوي على أي شيء حساس في المستودع.

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

الخلاصة

ثلاث تغييرات، اتجاه واحد: الفشل الذي كان يظهر لدى المستلم الآن يظهر لدى المُرسل. جدد الشهادة بدلاً من اللجوء إلى AllowExpired، اجعل SHA-256 هو الافتراضي، تحقق من المستندات الواردة تشفيرياً، واختر مستوى السجل قبل الحاجة إليه. العينة تُنفّذ جميع السلوكيات الستة في تمريرة واحدة، بما في ذلك الرفض، لذا يمكن تجربة الترقية في بضع دقائق.

موارد إضافية