שבועיים לפני כתיבת שורות אלו, כתבתי פוסט שעורר עניין על איך TLS 1.3 עובד כאשר השתמשתי ב-Wireshark כדי לעקוב אחר תהליך ה-Handshake הקריפטוגרפי שמתבצע בכל חיבור HTTPS. אני נורא אוהב שכותבים לי בתגובות פה או במייל שלי ומבקשים ממני פוסטים ואכן קורא באתר ביקש ממני להרחיב על נושא החתימה הדיגיטלית שהזכרתי כבדרך אגב. אז באמת זו הזדמנות מעולה. כדאי להכיר את זה גם כי באמת משתמשים בזה לא מעט פעמים בתהליכים שונים וגם בחיים האמיתיים – אנחנו מקבלים לפעמים מסמך חתום דיגיטלית ויהיה מעניין להכנס אל מאחורי הקלעים ולראות מה קורה.
אני מניח שיש לכם ידע בהצפנה אסימטרית. במידה ולא – תנו קפיצה לפוסט שלי על הצפנה אסימטרית לפרחחי ווב ותחזרו לפה עם הידע המתאים אחרי כמה דקות.
אז מה המטרה פה?
המטרה בחתימה דיגיטלית היא לבוא ולהצהיר שמשאב מסוים – תמונה, קובץ PDF, קובץ וורד, טקסט מסוים – הוא אמיתי ומקורי. בואו ונדגים עם קובץ PDF שיצרתי. אתם יכולים להוריד אותו פה.
א-ב-ל – איך אתם יכולים לדעת שהוא שלי באמת? טוב, הוא באתר שלי. אבל אם אני רוצה להפיץ אותו? אם יש בו גילויים או מידע או חוזה שעשיתי ואני באמת רוצה שידעו שהוא שלי מקורי ויהיה אפשר להוכיח את זה. שאף אחד אחר לא עשה בו שינוי ולו ברמה של ביט אחד. איך אני יכול לעשות את זה? התשובה היא חתימה. כשאני חותם על משהו – אני יכול להוכיח שהוא שלי. בלי אינטרנט, בלי שבועה על ספר תנ״ך ובלי טובות. רק עם קריפטוגרפיה.
איך חותמים?
ההליך הטכני של החתימה הולך ככה:
- אני יוצר hash – או פונקצית תמצות של הקובץ לפי אלגוריתם לבחירתי.
- את ה-hash אני מצפין עם המפתח הפרטי שלי. התוצאה של ההצפנה זו החתימה.
- מפיץ את הקובץ עם החתימה.
- משתמש שרוצה לוודא את האמינות של המסמך לוקח את המסמך ועושה לו hash עם האלגוריתם שלי.
- המשתמש לוקח את המפתח הפומבי שלי ופותח את החתימה, התוצאה היא hash. אם שני ה-hash דומים? החתימה מאומתת.
ההנחה פה שפונקצית התמצות תמיד מביאה ערך זהה למסמך אחד. שיניתי ביט אחד? אז פונקצית התמצות תביא ערך אחר לגמרי. כך בעצם גם מבטיחים שאיש לא שינה את הקובץ וגם מבטיחים אישור מקוריות. זו המשמעות של החתימה.
הדגמה! בפייתון
בואו ונעשה את זה. אני מדגים בפייתון אבל גם בג׳אווהסקריפט או בכל שפה אחרת אין בעיה כלל. השפה היא משנית. רק צריך להבין. בפייתון יש צורך להתקין ספריית קריפטוגרפיה צד שלישי, ב-Node.js יש כבר אחת כזו.
ראשית, ניקח קובץ. איזה קובץ? את ה-PDF שלי! ניצור תיקיה, נעתיק לתוכה את ה-PDF המהולל שלי. בואו נהפוך את התיקיה לפרוייקט פייתון. זה מאד קל עם uv שאני משתמש בו בכל הזדמנות.
uv init
uv venv
uv add cryptography
יאי! נכנס ל-main.py (שמי שיצר לנו אותו הוא uv init)
אני משתמש במפתחות הקריפטוגרפיים הרגילים שלי שנמצאים בתיקית ssh./~ שלי – מי שלא יודע איך ליצור מפתחות – אז צריך אותם כדי להתחבר לגיטהאב ולשרתים אחרים (גם לרספברי פיי ובפוסט הזה אפשר לקרוא עליהם.
החלטה נוספת שצריך זה האם לשמור את החתימה בפורמט בינארי או בפורמט טקסטואלי (base64). החתימה היא סדרת בייטים גולמיים – כלומר מידע בינארי וברירת המחדל היא לשמור את החתימה כקובץ בינארי והבייטים נשמרים ללא עיבוד נוסף, מה שמבטיח שלמות ודיוק בעת אימות החתימה. א-ב-ל אם יש צורך להעביר את החתימה באמצעי טקסטואלי (למשל דרך אימייל, JSON, או קובץ טקסט, קעקוע על גב של מישהו), אז אפשר להמיר את החתימה לייצוג טקסטואלי קריא שזה base64. בד״כ אני משתמש בבינארי – אבל יש אפשרות אחרת.
א��רי שקיבלתי את ההחלטות האלו, אני אגש אל ה-LLM הקרוב (בד״כ קופיילוט אבל כל אחד עם מה שבא לו) ואבקש ממנו. חשוב לפרט לו את המפתח ואת הפורמט שרוצים. משהו בסגנון:
כתוב קוד Python שיוצר חתימה דיגיטלית מאובטחת לקובץ PDF באמצעות מפתח פרטי קיים (למשל ~/.ssh/id_ed25519). הקוד צריך לטעון את המפתח הפרטי מהקובץ במערכת, להשתמש באלגוריתם Ed25519, ולחתום על תוכן הקובץ. לאחר החתימה, יש לשמור את החתימה בקובץ בינארי (signature.bin).
תקבלו משהו כזה:
import os
from cryptography.hazmat.primitives import serialization
# 1️⃣ Load the private key from ~/.ssh/id_ed25519
with open(f"{os.path.expanduser('~')}/.ssh/id_ed25519", "rb") as key_file:
private_key = serialization.load_ssh_private_key(key_file.read(), password=None)
# 2️⃣ Read the PDF data
with open("original-pdf-file.pdf", "rb") as f:
pdf_data = f.read()
# 3️⃣ Sign the data
signature = private_key.sign(pdf_data)
# 4️⃣ Save the signature to a file
with open("signature.bin", "wb") as sig_file:
sig_file.write(signature)
print("???? Signature saved to 'signature.bin'.")
אם תשמרו את הקובץ ב-sign.py ותריצו אותו עם uv run sign.py, אכן תיווצר חתימה!
אבל רגעעעע מה האלגוריתם? ובכן, האלגוריתם נקבע לפי המפתח שלי כמובן. במקרה הזה זה EdDSA עם Ed25519. רוצים משהו אחר? צרו מפתח מתאים. מה כדאי לבחור? Ed25519 הוא די סטנדרטי בשעת כתיבת שורות אלו, אבל יש שיקולים קריפטוגרפיים אחרים ומומלץ לשאול קריפטוגרף.
וידוא החתימה
הקוד של החתימה פשוט למדי, אבל אם אני אשתמש במפתח הפומבי ״הרגיל״ שלי, אני אתקל בבעיה כי ספרית הקריפטוגרפיה של פייתון דורשת פורמט מאד מסוים והיא לא מעט פעמים מציקה גם במפתחות תקינים לחלוטין. יש לנו שתי אפשרויות – הראשונה להמיר את המפתח הציבורי שלי לפורמט pem והשניה היא להשתמש בספרית pynacl כדי שתבצע את ההמרה on the fly. במה לבחור? תלוי בכם! אני כן אשתמש ב-pynacl:
import os
import base64
from nacl.signing import VerifyKey
from nacl.exceptions import BadSignatureError
# 1️⃣ Read the PDF data
with open("original-pdf-file.pdf", "rb") as f:
pdf_data = f.read()
# 2️⃣ Read and decode the SSH public key
with open(os.path.expanduser("~/.ssh/id_ed25519.pub"), "r") as f:
pubkey_line = f.read().strip()
# Extract base64-encoded key blob
key_base64 = pubkey_line.split()[1]
key_blob = base64.b64decode(key_base64)
# Extract the raw 32-byte Ed25519 key from the SSH key blob
offset = 4 + len("ssh-ed25519")
pubkey_length = int.from_bytes(key_blob[offset:offset+4], "big")
raw_key = key_blob[offset+4:offset+4+pubkey_length]
# 3️⃣ Create VerifyKey
verify_key = VerifyKey(raw_key)
# 4️⃣ Read the signature
with open("signature.bin", "rb") as f:
signature = f.read()
# 5️⃣ Verify the signature
try:
verify_key.verify(pdf_data, signature)
print("✅ Signature is valid!")
except BadSignatureError:
print("❌ Signature verification failed!")
אם תריצו את זה עם uv run verify-sign.py, תגלו ש-Yay! החתימה תקינה לגמרי! כלומר גם המקור תקין (לפי המפתח הציבורי שלי) וגם המידע עבר ולידציה
עכשיו, נדמיין שיש איזו פורצת נועזת שרוצה להכניס שני רווחים בביט או שני ביטים בתחילת הקובץ כדי לפרוץ (יש פה רפרנס!) או להרוס את הקובץ. זה מה שטוב בחתימה – תשנו ביט אחד, או שני ביטים – החתימה תכשל ואז נדע שמישהו התעסק עם הקובץ ונוכל להמנע מפורצים נועזים מהרשת האפלה.
(echo -ne '\x00\x01'; cat original-pdf-file.pdf) > temp && mv temp original-pdf-file.pdf
אם נריץ עכשיו את הבדיקה. היא תיכשל!
סיכום
חתימה אלקטרונית מוודאת גם Authenticity – כלומר שמי שאומר שיצר את המסמך באמת יצר אותו (הוא לא יוכל להכחיש אחר כך, כי זה המפתח הפרטי שלו שביצע את החתימה!) וגם Integrity – שאף אחד לא נגע ולו בביט אחד של המסמך שנחתם.
כל אחד יכול לחתום ולוודא חתימה, אבל נדרש ידע טכני והבנה (שלכם יש עכשיו!) וגם נדרש לפרסם את המפתח הפומבי באופן פומבי מספיק כך שתוקף לא יוכל להתחזות. כיוון שזה מורכב, יש ספקיות חתימה דיגיטליות בעולם ובישראל שמשתמשות באותו תהליך בדיוק אבל הן מוסיפות חותמת של מכובדות ומקילות את העול התפעולי. הן מתחייבות שהמפתח הציבורי שהן מפרסמות באמת שייך לאותו גורם ולא לתוקף אלמוני ומונעות את ההתעסקות התפעולית והאבטחתית. מאחורי הקלעים? זה בדיוק מה שהן עושות.
3 תגובות
הצעה לפוסט הבא – איך מדינת ישראל (אולי גם מדינות אחרות, אין לי ניסיון איתם) מחפפת ולא מבינה שום דבר בעולם החתימות הדיגיטלי.
א. כבר שנים שחברות מורשות לשלוח חשבוניות "חתומות דיגיטלית", זה כביכול אמור להיות פתרון הרבה יותר טוב מסתם חשבונית, אבל אין שום דרישות וממילא כמובן שום פיקוח על ההטמעה שלהם בתוכנות השונות.
התוכנות לא מחויבות להשתמש בספקים מורשים, וממילא אין שום פיקוח שלא ביצעת שינוי בחשבונית, וגם לא אימות לתאריך.
ב. בהרבה חברות – מנהל החשבונות פשוט מדפיס את החשבונית הדיגיטלית ומקטלג. אף אחד מהצדדים – כולל המבקר של מס הכנסה – לא מבין שאין שום משמעות לחתימה דיגיטלית על נייר.
ג. במקום להטמיע פתרון מבוסס PKI – רשות המיסים המציאו תהליך חדש, יקר בטירוף (פיתוח, הטמעה, תחזוקה, מצד רשות המיסים + ספקי תוכנה + חברות ועצמאיים), מסורבל, והזוי שכל חשבונית מעל 15K צריכה להנפיק מספר אימות בזמן אמת https://govextra.gov.il/taxes/innovation/home/israel-invoices/
ד. כל אזרח ישראלי בעל תעודת זהות ביומטרית מחזיק בכיס private key מאומת על ידי הממשלה, למרות זאת – חוץ מהזדהות מול gov.il אי אפשר לעשות איתו שום דבר.
1. אתה רוצה לשלם מעמ אונליין? לך תוציא (ותשלם) כל 4 שנים מפתח חדש מ comsign
2. אתה רוצה להזדהות כעורך דין בנט המשפט? לך תוציא מפתח נוסף שונה.
3. אתה רוצה להזדהות מול רשות ניירות ערך כדי לקבל API לנתוני הבנק שלך ושל הלקוחות? בשמחה, רק תוציא מפתח נוסף ייעודי, שלם 16,000 שח, ונדע שאתה באמת אתה.
4. לעומת זאת אם אתה קונה דירה – תצטרך לחתום על 500 ניירות ידני, או שהקבלן ישלם עוד כמה אלפים ל comsign עבור תוכנה שמעתיקה את הקשקוש שלך מהטבלט למסמך ומצרפת אליה חתימה דיגיטלית של עורך הדין (בתנאי כמובן שיש לו מפתח של comsign)
טוב אולי יצא סדרת כתבות – אשמח לעזור אם צריך… אני פוגש את זה כל כמה זמן במקום נוסף ומתעצבן.
נ.ב. אני לא אשאל למה ביטוח לאומי לא מסוגל לעבור לעבוד מול מערכת ההזדהות הממשלתית…
לא מדוייק.
באמת רשות המסים וביטוח לאומי חיים בפלנטה אחרת מהשאר..
אבל
א. עם אותו כרטיס של קומסיין אתה יכול להזדהות בכל המקומות ולא נדרש כרטיס נפרד לכל מקום.
ב. נ"ט המשפט היום ניתן להזדהות עם ביומטרי או כניסה דרך הזדהות אישית ממשלתית. לאט לאט כל המערכות של המדינה עוברות לזיהוי דרך הזדהות אישית או ביומטרית מתי זה יחלחל למס הכנסה והמלל זה כבר שאלה טובה
זה נהדר, תודה!
אני רק עכשיו שמתי לב למשהו: אתה נותן הוראות ל-LLM בעברית? כלומר, זה מקובל? והאם יש הבדל כלשהו ברמת הביצועים שלו לעומת פרומפטים באנגלית?