💡 דוגמה מלאה עובדת זמינה ב‑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 – החבילה כוללת זמן ריצה של .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 במקום גלובלית. החריגה זולה; אצווה של חתימות לא תקפות אינה.
- תעדו את טקסט האזהרה, לא רק מונה. הוא מציין את האישור ואת התאריך, וזה החלק היחיד שמפעיל יכול לפעול עליו.
- בדקו את השעון לפני שמאפשרים אישור שעדיין לא בתוקף. האישור בדרך כלל נכון והמערכת שגויה, וזה משפיע על יותר מקריאה אחת.
- הוציאו עקבות מהייצור. כ‑עשר לכל ריצת חתימה מצטבר מהר; הפעלו רק לאבחון וכבו אחרי.
- אמתו אחרי החתימה בכל צינור, עכשיו שהבדיקה קריפטוגרפית, כך פלט פגום נתפס לפני שהמקבל מגלה זאת.
סיכום
שלוש בקרות, קריאת חתימה אחת, ורעיון העיצוב המשותף לכולן: תוצאה מסוכנת דורשת כעת החלטה, ותוצאה בטוחה אינה דורשת דבר. שמרו על בדיקת התוקף, התייחסו ל‑allow_expired כחריגה per‑call שמדווחים עליה, אל תיגעו בדיפסט אלא אם מדיניות דורשת אחרת, וקבעו רמת יומן שמאפשרת קריאת האזהרות.
הרצת הדוגמה על אחד קבצי ה‑PDF שלכם לוקחת דקה ומדפיסה בדיוק מה כל בקרה שינתה – שש קבצים חתומים, סירוב מודע אחד, ושלוש שורות של ספירות הודעות שכעת אינן זהות.