💡 مثال کامل کارآمد در گیت‌هاب موجود است:
امضای-PDF-با-Vault-Transit-در-Python

چرا امضای خودکار در بررسی نگهداری کلید شکست می‌خورد

تیم‌های انطباق و عملیات مالی اسناد را به تعداد هزاران بار امضا می‌کنند: قراردادهای وام، صورت‌حساب‌ها، بسته‌های سیاستی، نامه‌های حسابرسی. تقریباً هیچ‌یک از این‌ها توسط شخصی که دکمه‌ای را می‌زند امضا نمی‌شود. یک سرویس این کار را انجام می‌دهد، به‌صورت زمان‌بندی شده، و گواهی‌نامهٔ پیوست‌شده به آن اسناد نام سازمان را حمل می‌کند. این باعث می‌شود کلید امضا یکی از حساس‌ترین مواردی باشد که شرکت در اختیار دارد و معمولاً در یک فایل PFX در کنار برنامه‌ای که از آن استفاده می‌کند، قرار می‌گیرد.

امضای خارجی یک قابلیت GroupDocs.Signature برای Python از طریق .NET است که مقدار هش سند را به کدی که شما فراهم می‌کنید می‌فرستد به‌جای امضای آن با کلید خصوصی که برنامه در اختیار دارد. این قابلیت به این دلیل وجود دارد که ساختار PFX تحت بررسی دقیق دوام نمی‌آورد. یک فایل یک کپی است و کپی‌ها پخش می‌شوند: به داخل تصویر کانتینر، خط لولهٔ استقرار، نسخهٔ پشتیبان، لپ‌تاپ توسعه‌دهنده‌ای که به‌اخیرین به‌یک مشکل تولیدی اشکال زدایی کرده بود. هر کسی که یکی از این کپی‌ها و رمز عبور را داشته باشد می‌تواند به‌نام شما امضا کند تا زمانی که کسی متوجه شود و گواهی‌نامه را لغو کند.

آنچه این تیم‌ها نیاز دارند یک فرمت فایل متفاوت نیست. آن‌ها نیاز دارند کلید دقیقاً در یک مکان وجود داشته باشد، با ردپای حسابرسی برای هر بار استفاده، و سرویس امضایی که بتواند درخواست امضا کند بدون این‌که هرگز به کلید اعتماد داشته باشد. این همان کاری است که یک مدیر کلید مانند HashiCorp Vault برای آن وجود دارد و تنها زمانی مفید است که کتابخانهٔ اسناد بتواند به‌صورت واگذار شدهٔ رمزنگاری را انجام دهد.

چرا یک فایل گواهی‌نامهٔ محافظت‌شده با رمز عبور کافی نیست

تمایل این است که PFX را قفل کنیم: محدود کردن دسترسی‌های فایل، ذخیرهٔ رمز عبور در یک مدیر راز، رمزنگاری حجم. هر قدمی کمک می‌کند اما هیچ‌یک از آن‌ها واقعیت اصلی را تغییر نمی‌دهد. در لحظهٔ امضا، کلید خصوصی در فرآیند بارگذاری می‌شود، بنابراین یک dump حافظه، یک فرار از کانتینر یا یک عبارت لاگ بی‌دقت می‌تواند هنوز آن را فاش کند. کنترلی که می‌خواهید، کلیدی است که اصلاً قابل خواندن نباشد، نه کلیدی که خواندن آن سخت‌تر باشد.

موتور 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 تولیدی اشاره می‌کند.

ساخت گام به گام

گام ۱ – خواندن کلید امضا از 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}")

خواندن نسخه یک‌بار هدف این گام است. یک گواهی‌نامه یک کلید عمومی دارد، بنابراین گواهی‌نامه و هر امضا باید از همان نسخه باشند.

گام ۲ – ساخت گواهی‌نامه‌ای برای کلیدی که هیچ‌کس نمی‌تواند استخراج کند

یک امضای 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 به‌عنوان منقضی یا هنوز معتبر گزارش می‌شود.

گام ۳ – امضای 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 نوشته می‌شود.

درون امضاکننده، هش با prehashed = true و pkcs1v15 به Vault می‌رود. این دو فیلد تعیین می‌کنند امضا معتبر باشد یا نه و اولین مورد در صورت عدم وجود به‌صورت ساکت شکست می‌خورد.

گام ۴ – تأیید قبل از خروج هر چیزی از سرویس

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 و پین‌گذاری نسخه یکسان بودند. تیم‌ها در حوزهٔ بهداشت، دولت و بیمه به دلایل مشابه مالی از آن استفاده می‌کنند: کلید باید در جایی باشد که بتواند ثابت کند برای چه استفاده شده است.

شروع کار

منابع