💡 דוגמה מלאה עובדת זמינה ב‑GitHub:
sign-docx-with-mldsa-certificates-python

מבוא

חתום על חוזה אחר הצהריים עם RSA‑2048 והבטחתך חייבת להחזיק כל עוד החוזה חשוב. אם זה עשרים או שלושים שנה – ובמקרים של מסמכי נדל״ן, טפסי הסכמה והסכמי הנדסה זה קורה לעיתים קרובות – ההבטחה חייבת לשרוד את האלגוריתם. ההתקפה אינה חייבת להתקיים היום; היא צריכה להתקיים לפני שהמסמך מפסיק להיות רלוונטי, ואז כל מי שמחזיק במפתח הציבורי יכול לגזור את הפרטי ולחתום בשם שלך.

חתימה על מסמכים לאחר‑קוואנטית היא תכונת GroupDocs.Signature עבור Python שמחליפה את ההבטחה באלה המבוססת על ML‑DSA, אלגוריתם החתימה שהתקן NIST הפך ל‑FIPS 204 בשנת 2024. תמיכה בפורמט Word הגיעה ב‑GroupDocs.Signature 26.9, והיא משתמשת באותו API שכבר יש לך: מפתח ML‑DSA נמצא בקובץ PFX ונכנס ל‑DigitalSignOptions בדיוק כמו מפתח RSA.

המדריך הזה חותם קובץ DOCX בארבעה שלבים, משווה את שלושת רמות האבטחה על פי פלט מדוד, מאמת את החתימה רק עם תעודה ציבורית, ומסיים עם שני המגבלות שכדאי לדעת לפני שמתחילים.

למה זה חשוב יותר מההגירה הרגילה

הגירת חתימות שונה מהגירת הצפנה באספקט אחד שמקל לדחות אותה ומקשה לתקן אותה.

בהצפנה, בעיית “קצור‑עכשיו‑פענח‑מאוחר יותר” היא מיידית: כל דבר שנלכד היום ניתן לאחסן ולפתוח מאוחר יותר. בחתימות, שום דבר שכבר חתמת עליו לא ניתן לזייף רטרואקטיבית – אבל גם שום דבר שחתמת עליו אינו מוכח כשלך ברגע שהמפתח ניתן לגזירה מהתעודה שכל אחד מחזיק בה. החתמת מחדש של עשור של מסמכים מאוחסנים עם מפתחות חדשים אפשרית ואף אף אחד לא רוצה להיות זה שמתכנן זאת.

לכן העצה המעשית היא ממוקדת ולא כללית: הגר את המסמכים שההחזקתם ארוכה, השאר את השאר. כמה פרופילים כבר קבעו את הסטנדרט – CNSA 2.0 דורש ML‑DSA‑87 למערכות בטחון לאומי – ולשאר המשתמשים גורם ההחלטה הוא כמה זמן הקובץ צריך להישאר ניתן להגנה.

דרישות מוקדמות

  • Python 3.9 או גרסה מאוחרת יותר על מפרש 64‑bit – החבילה כוללת סביבת ריצה של .NET משולבת ואין לה גלגל 32‑bit
  • GroupDocs.Signature עבור Python דרך .NET 26.10.0, עם רישיון זמני חינמי להסרת מגבלות ההערכה
  • תעודת ML‑DSA כמסמך PFX מוגן בסיסמה, וקובץ Word לחתימה

התקנה

pip install groupdocs-signature-net

שלב 1 – חתימה עם תעודת ML‑DSA

התעודה עושה את העבודה. הקריאה היא בדיוק כמו שהיית כותב עבור RSA:

with signature.Signature(source_path) as sign:
    options = DigitalSignOptions(pfx_path)
    options.password = CERTIFICATE_PASSWORD

    result = sign.sign(output_path, options)

זהו כל סיפור האימוץ עבור קוד שכבר חותם: מצביעים DigitalSignOptions על קובץ PFX שונה. אין אפשרות חדשה, אין פרמטר אלגוריתם נפרד, ואין סניף עבור לאחר‑קוואנטי.

קריאת החותם חזרה דורשת שלב נוסף, ומכילה את המלכודת הספציפית ל‑Python בתרגיל כולו:

for created in result.succeeded:
    certificate = getattr(created, "certificate", None)
    subject = getattr(certificate, "subject", None)
    if subject:
        return str(subject)

התעודה על DigitalSignature היא אובייקט גשר שמפענח תכונות דינמית. certificate.subject מחזיר CN=GroupDocs.Signature MLDSA65 test, בעוד ש‑dir() על אותו אובייקט אינו מציג דבר. בחנתי זאת עם dir() תחילה, סיכמתי שה‑subject אינו חשוף, והייתי בטעות – ולכן אם תבצע אינטורספקציה לפני הקריאה, תדלג על ערך שקיים בפועל.

שלב 2 – השוואת שלוש רמות האבטחה

ML‑DSA מגיע בשלושה סטים של פרמטרים, והן נבחרות על‑ידי העברת תעודה שונה:

levels = (
    ("ML-DSA-44", MLDSA44_PFX),
    ("ML-DSA-65", MLDSA65_PFX),
    ("ML-DSA-87", MLDSA87_PFX),
)

for level, pfx_path in levels:
    with signature.Signature(source_path) as sign:
        options = DigitalSignOptions(pfx_path)
        options.password = CERTIFICATE_PASSWORD
        sign.sign(output_path, options)

    sizes[f"{level} -> {file_name}"] = os.path.getsize(output_path)

זהו השלב שכדאי להריץ בפועל, מכיוון שהמסחר הוא בדרך כלל מתואר אך נדיר למדוד. מתוך חוזה מקור בגודל 132 KB:

רמה קטגוריית אבטחה של NIST קובץ חתום מעל הקטן ביותר
ML-DSA-44 2 138,202 בתים -
ML-DSA-65 3 140,650 בתים +2,448 בתים
ML-DSA-87 5 143,971 בתים +5,769 בתים

מתחת ל‑6 KB מפרידים את הרמה החלשה מהחזקה. בחוזה זה זה לא משנה, ולכן ההחלטה מתבהרת: השתמש ב‑ML‑DSA‑65 כברירת מחדל, ב‑ML‑DSA‑87 כאשר פרופיל דורש קטגוריה 5 או כאשר הגודל אינו רלוונטי, וב‑ML‑DSA‑44 רק כאשר אתה חותם על כל כך הרבה קבצים שהקילובייטים מצטברים למשהו משמעותי.

שלב 3 – אימות עם תעודה ציבורית

המקבל צריך רק את תעודת הציבור של החותם ולא שום סוד:

with signature.Signature(signed_path) as sign:
    options = DigitalVerifyOptions(certificate_path)
    if password is not None:
        options.password = password

    return sign.verify(options).is_valid

הדוגמה קוראת לפונקציה זו פעמיים על אותו קובץ: פעם אחת עם mldsa65.cer, החצי הציבורי של מפתח החתימה, ופעם שנייה עם PFX של חותם שונה. הראשונה מחזירה True, השנייה False. שים לב שהתעודה השגויה מחזירה False במקום לזרוק – “חתום על ידי מישהו אחר” הוא תשובה שהקוד שלך צריך לטפל בה, לא חריגה. הבדיקה מכסה את תוכן המסמך יחד עם מספר הסדרה והטביעת אצבע של התעודה, ולכן קובץ שנערך אחרי החתימה גם הוא נכשל.

שלב 4 – קריאת החתימות מתוך מסמך

כאשר מסמך חתום מגיע ואתה לא יודע איזו תעודה לצפות לה:

with signature.Signature(signed_path) as sign:
    found = sign.search(SignatureType.DIGITAL)
    for item in found:
        print(item.sign_time, item.is_valid)

search עם SignatureType.DIGITAL מחזיר אובייקטים מסוג DigitalSignature שנושאים את התעודה, זמן החתימה ודגל תקפות. קובץ Word יכול להכיל מספר חתימות, כולל תערובת של RSA ו‑ML‑DSA, וכל אחת מדווחת עם התעודה והתקפות שלה.

האם זה משנה איך מקבלים מאמתים?

לא בשום אופן שהם ירגישו. המקבל עדיין צריך רק את תעודת הציבור של החותם, עדיין מעביר אותה ל‑DigitalVerifyOptions זהה, ועדיין מקבל ערך בוליאני בחזרה. שום דבר בנתיב האימות אינו ספציפי ל‑ML‑DSA. המקום היחיד שבו האלגוריתם מתגלה הוא במדד החתימה של Microsoft Word עצמו, שעשוי עדיין לא לזהות ML‑DSA מכיוון שהפורמט חסר מזהה סטנדרטי עבורו.

יישומים בעולם האמיתי

חוזים עם שמירת ארכיון ארוכה

המקרה הברור ביותר. מסמך שצריך להישאר ניתן לאימות במשך עשורים נחתם פעם אחת, עכשיו, עם ML‑DSA‑65 או ML‑DSA‑87, ולעולם לא נדרש לחתום מחדש מכיוון שהאלגוריתם שלו אינו מתיישן.

סביבות מוסדרות עם פרופיל מוגדר

כאשר CNSA 2.0 או פרופיל דומה חלים, הרמה אינה החלטה – ML‑DSA‑87 הוא הדרישה, והשאלה ההנדסית היחידה היא האם הפורמט נתמך.

צינורות מעורבים במהלך ההגירה

חתימה על מסמכים חדשים לאחר‑קוואנטית תוך השארת הארכיון ללא שינוי היא מצב ביניים סביר לחלוטין, ו‑search המדווח על כל חתימה בנפרד הוא מה שמאפשר לנהל זאת.

שיטות עבודה מומלצות וטיפים

  • הגר לפי משך השמירה, לא לפי נפח. המסמכים שזקוקים לכך הם אלו שחיים זמן רב; קבלה שמחזיקה רק 90 יום אינה דורשת זאת.
  • השתמש ב‑ML‑DSA‑65 כברירת מחדל אלא אם פרופיל מציין רמה אחרת, ואל תדאג להבדל בגודל – הוא מתחת ל‑6 KB לכל חתימה.
  • שמור על RSA כאשר המקבל מאמת ב‑Word. חתימות נכונות שהקורא מדגיש הוא מצב גרוע יותר מהגירה איטית.
  • החלף את תעודות המבחן. קבצי ה‑PFX של הדוגמה נחתמו עצמאית עם סיסמה פומבית, ולכן כל חתימה איתם אינה מוכיחה דבר.
  • אמת אחרי החתימה בכל צינור, באמצעות תעודת הציבור שהמקבל היה מקבל.

פתרון בעיות נפוצות

Microsoft Word אינו מציג את החתימה כתקפה. מצופה לעת עתה: אין מזהה XML‑DSig סטנדרטי ל‑ML‑DSA, ולכן Word עשוי לא לזהות אותה למרות שהחתימה נכונה ו‑GroupDocs.Signature מאמתת אותה. אמת בצינור שלך, ושמור על RSA למסמכים שהמקבלים מסתמכים על מדד Word.

קריאת החתימה נדחית עבור PDF או גיליון אלקטרוני. חתימה ב‑ML‑DSA מכסה פורמטים של Word – DOCX, DOC, ODT ועוד. PDF, גיליונות והצגות עדיין אינם נתמכים, ויש להמשיך להשתמש ב‑RSA או ECDSA כפי שהיה לפני.

נושא התעודה חוזר ריק. כמעט תמיד מדובר במלכודת dir() משלב 1: התכונה נפתרת דינמית, ולכן יש לקרוא אותה במקום לבדוק קיומה מראש.

סיכום

שינוי הקוד הוא שינוי בתעודה, וזה החלק שהופך את הפעולה לכדאית לפני שהיא דחופה. חתום על מסמכי Word ארוכי‑חיים עם ML‑DSA‑65, השתמש ב‑ML‑DSA‑87 כאשר פרופיל דורש זאת, אמת עם תעודת הציבור, ושמור על RSA כאשר הפורמט או הקורא דורשים זאת.

הפעל את הדוגמה על אחד מהחוזים שלך והשלושה גדלים יגידו לך, בבייטים, בדיוק מה העלות של הרמה החזקה ביותר. בקובץ שבדקתי העלות הייתה 5,769 בתים.

משאבים נוספים