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

لماذا يفشل التوقيع الآلي في مراجعة حراسة المفتاح

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

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

ما تحتاجه هذه الفرق ليس تنسيق ملف مختلف. إنهم يحتاجون إلى وجود المفتاح في مكان واحد فقط، مع سجل تدقيق لكل مرة يُستخدم فيها، وخدمة توقيع يمكنها طلب توقيع دون أن تُوثق بالمفتاح. هذا ما يُستخدم فيه مدير مفاتيح مثل HashiCorp Vault، ولا يساعد إلا إذا كان بإمكان مكتبة المستندات أن تُفوض التشفير.

لماذا ملف شهادة محمي بكلمة مرور ليس كافيًا

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

محرك Transit في Vault يوفّر ذلك. فهو يُجري التشفير كخدمة: ترسل قيمة، يوقع أو يُشفّر بالمفتاح الذي يحتفظ به، ويعيد النتيجة. المفاتيح التي تُنشأ داخله يمكن وضع علامة غير قابلة للتصدير، ولا يخزن ما يتلقاه. يغطي GroupDocs.Signature النصف الآخر، لأن DigitalSignOptions.custom_sign_hash يتيح لك ربط الموقّع الخاص بك بخط أنابيب توقيع PDF.

الإعداد: GroupDocs.Signature للغة Python مع Vault Transit

يقوم GroupDocs.Signature بكل شيء حتى التشفير. يفتح ملف PDF، يحدد أي نطاق بايت يغطيه التوقيع، يحسب تجزئة ذلك النطاق، وينادي موقّعك مع التجزئة. يرسل موقّعك التجزئة إلى Vault ويعيد البايتات التي ينتجها Vault. تُدمج المكتبة هذه البايتات مع الشهادة وتكتب PDF موقعًا عاديًا.

تحتاج إلى Python 3.11 أو 3.13، وحزم groupdocs-signature-net و asn1crypto، وDocker لحاوية Vault في وضع التطوير. الترخيص لـ GroupDocs اختياري؛ بدون ترخيص، وضع التقييم يقبل مستندات تصل إلى صفحتين. المستودع النموذجي يعمل من البداية إلى النهاية بأمرين، والكود نفسه يشير إلى Vault إنتاجي بتغيير متغيرين بيئيين.

بناءه خطوة بخطوة

الخطوة 1 - قراءة مفتاح التوقيع من Vault

يطلب التطبيق من Vault المفتاح المسموح باستخدامه، ويحصل على رقم نسخة ومفتاح عام. النصف الخاص لا يكون أبدًا جزءًا من الرد.

key = vault.read_key(KEY_NAME)
if key is None:
    # أول تشغيل: تمكين Transit وإنشاء مفتاح RSA-2048.
    vault.enable_transit()
    vault.create_key(KEY_NAME, "rsa-2048")
    key = vault.read_key(KEY_NAME)
print(f"Signing key: '{KEY_NAME}', version {key.version}")

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

الخطوة 2 - بناء شهادة لمفتاح لا يمكن لأحد تصديره

يحمل توقيع PDF شهادة X.509 حتى يتمكن المُتحقق من العثور على المفتاح العام. مع مفتاح غير قابل للتصدير، تُجمع الشهادة حول النصف العام وتُوقع بواسطة Vault.

tbs_certificate = x509.TbsCertificate({...})   # مبنية حول المفتاح العام في Vault

# احسب التجزئة محليًا، ثم دع Vault يوقع تجزئة الشهادة نفسها.
digest = hashlib.sha256(tbs_certificate.dump()).digest()
signature = vault.sign_hash(key_name, key.version, digest, "sha2-256")

certificate = x509.Certificate({
    "tbs_certificate": tbs_certificate,
    "signature_algorithm": algorithm,
    "signature_value": signature,
})

تبدأ الشهادة عمدًا يومًا واحدًا قبل الآن. قارئ PDF يقارن وقت التوقيع مع تاريخ البدء، وعلى جهاز يقع ساعتين شرق UTC تم الإبلاغ عن شهادة صُدّرت قبل ثوانٍ من التوقيع بأنها منتهية أو غير صالحة بواسطة Adobe Acrobat.

الخطوة 3 - توقيع PDF عبر الموقّع الخارجي

certificate_object = DigitalVerifyOptions(
    certificate_stream=io.BytesIO(certificate)).certificate
digital_signature = DigitalSignature(certificate=certificate_object)
digital_signature.sign_time = datetime.now().astimezone()

options = DigitalSignOptions()
options.signature = digital_signature
options.custom_sign_hash = VaultSigner(vault, KEY_NAME, key.version)
options.hash_algorithm = HashAlgorithm.SHA256
options.reason = "Document signed with HashiCorp Vault Transit"

with gs.Signature(str(input_pdf)) as signature:
    signature.sign(str(output_pdf), options)

سطران مخصصان للغة Python. لا يحتوي الربط على نوع X.509، لذا يُسترجع كائن الشهادة من DigitalVerifyOptions؛ تمرير مسار إلى DigitalSignOptions يسبب الخطأ “Specified certificate doesn’t contain private key”. وsign_time مُعتمد على المنطقة الزمنية عن قصد، لأن قيمة مع إزاحة تُكتب في PDF معها.

داخل الموقّع، تُرسل التجزئة إلى Vault مع prehashed = true وpkcs1v15. هذان الحقلان يحددان ما إذا كان التوقيع صالحًا، والفشل الأول يحدث بصمت عندما يكون مفقودًا.

الخطوة 4 - التحقق قبل أن يغادر أي شيء الخدمة

options = DigitalVerifyOptions(certificate_stream=io.BytesIO(certificate))
with gs.Signature(str(signed_pdf)) as signature:
    is_valid = signature.verify(options).is_valid

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

الأثر التجاري: ما الذي يتغير في التدقيق

القلق PFX على مضيف التطبيق التوقيع عبر Vault Transit
نسخ المفتاح الخاص نسخة لكل مضيف، صورة، نسخة احتياطية وحاسوب محمول نسخة واحدة، داخل Vault
الصلاحيات التي يحتاجها تطبيق التوقيع وصول كامل إلى ملف المفتاح قراءة المفتاح، استدعاء transit/sign
دليل أن المستند تم توقيعه سجلات التطبيق، إذا أضافها أحد سجل تدقيق واحد في Vault لكل توقيع
الاستجابة لتسريب مشتبه به إلغاء، إعادة إصدار، إعادة نشر في كل مكان تدوير المفتاح في Vault، إعادة إصدار مرة واحدة
من يمكنه التوقيع باسمك أي شخص يملك الملف وكلمة المرور أي شخص يحمل رمز Vault صالح

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

سير العمل الحقيقي: اتفاقية قرض موقعة بواسطة خدمة

منصة إقراض تُنشئ اتفاقية PDF عندما يُوافق على طلب، تُوقعها، وتضعها في مخزن مستندات العميل. قبل هذا التغيير، كانت خدمة التوقيع تحتفظ بشهادة الشركة وكان دليل تشغيل النشر يحتوي خطوة لنسخ الـ PFX إلى مضيفين جدد. بعد التغيير، تحتفظ الخدمة برمز AppRole بصلاحيتين، ولم يُوجود المفتاح الخاص للشهادة إلا داخل Vault.

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

ما الذي يجب أن أضبطه أولًا؟

معالجة التجزئة. اسم خوارزمية خاطئ يفشل بصوت عالٍ، حيث يرد Vault بـ 500 crypto/rsa: input must be hashed message، ويكلف دقيقة. عدم وجود علم prehashed لا ينتج عنه أي خطأ: يقوم Vault بتجزئة التجزئة مرة أخرى، يغطي التوقيع قيمة خاطئة، ويظهر PDF مكتملًا. تحقق من كل ملف توقّعه وأن هذا الفشل لا يبقى غير مرئي.

أماكن أخرى يناسب هذا التصميم

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

البدء

الموارد