💡 דוגמה מלאה עובדת זמינה ב‑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 הוא אופציונלי; ללא רישיון, מצב ההערכה מקבל מסמכים של עד 2 עמודים. מאגר הדוגמה פועל מקצה לקצה בשתי פקודות, והקוד עצמו מצביע על Vault ייצור על ידי שינוי שני משתני סביבה.
בנייה שלב אחר שלב
שלב 1 - קריאת מפתח החתימה מ‑Vault
היישום מבקש מ‑Vault את המפתח שהוא מורשה להשתמש בו, ומקבל בחזרה מספר גרסה ומפתח ציבורי. החצי הפרטי אף פעם אינו חלק מהתשובה.
key = vault.read_key(KEY_NAME)
if key is None:
# First run: enable Transit and create an RSA-2048 key.
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({...}) # built around the Vault public key
# Hash locally, then have Vault sign the certificate's own digest.
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 והקיבוע לגרסה היו זהים. צוותים בתחום הבריאות, הממשלה והביטוח פונים אליו מאותה סיבה שהפיננסים עושים: המפתח צריך להיות במקום שיכול להוכיח למה הוא שימש.
התחלה
- התחלה חינם: קבל רישיון ניסיון, גישה מלאה ל‑API
- קרא את מדריך האינטגרציה: חתימת PDF עם מפתח Vault Transit ב‑Python
- עיין בקוד: vault-transit-pdf-signing-python
- שאל שאלות יישום בפורום הקהילה
משאבים
- תיעוד GroupDocs.Signature עבור Python דרך .NET
- תיעוד API
- רישיון ניסיון חינמי
- [פרויקט לדוגמה ב‑GitHub][GITHUB_URL]
- [פורום תמיכה][FORUM_URL]