רשומה חתומה מוכיחה משהו רק אם מישהו אחר יכול לבדוק אותה בקוד שלא קיבל מכם. מה מצאתי כשכתבתי verifier שני למפרט attestation פתוח. ועוד שני issues פתוחים בפרויקט שלי, שנכשל באותו מבחן.
לחתום על רשומה זה קל. מפתח, hash, כמה עשרות שורות קוד.
ואז מגיע המשפט הזה. "כל אחד יכול לאמת את זה."
כל אחד יכול. מריצים את ה-verifier שאתם כתבתם, מול הפורמט כמו שהבנתם אותו. זו דעה אחת, שבדק אותה מי שכתב אותה.
המשפט הזה מופיע ב-README של הפרויקט שלי. בפרויקט יש verifier אחד, והוא נמצא בחבילה שכותבת את ה-receipts.
שני דברים הופכים דעה לראיה. test vectors קבועים, ו-verifier שני שנכתב לפי המסמך, בלי לייבא את הראשון. הפוסט הזה על שניהם.
מה יש בסט טוב של test vectors
שני issues פתוחים בפרויקט שלי, שנכשל במבחן הזה היום
1. ה-verifier שמסכים עם עצמו
במערכת ראיות רגילה יש שלושה דברים: תיאור של הפורמט, קוד שכותב רשומות וקוד שמאמת אותן. שני האחרונים יושבים בחבילה אחת וחולקים פונקציות עזר. הטסטים חותמים על רשומה ומאמתים אותה. עוברים.
הם היו עוברים גם אם התיאור והקוד לא היו מסכימים זה עם זה. שני הצדדים של הטסט משתמשים באותה פונקציית canonicalization, באותו decoder של base64, באותה הבנה של מה זה מספר שלם. באג בפונקציית עזר משותפת לא נראה, כי הוא מופעל בשני הצדדים.
מי שמקבל את הרשומה נמצא במצב גרוע יותר. יש לו תיאור ויש לו תוצאה. אין לו דרך לדעת אם השניים מסכימים, ואין לו קלטים קבועים לנסות עליהם את הקוד שלו.
2. מה מצא מימוש שני
בסתיו הזה תרמתי ל-TRACE, מפרט attestation פתוח למערכות של סוכנים. עשרה pull requests שלי מוזגו. אחד מהם הוא verifier שני, ב-TypeScript. הוא נכתב לפי המפרט ונבדק מול ה-reference ב-Python עם differential harness. אותן רשומות נכנסות לשניהם, ומשווים את התוצאות.
בריצה הנוכחית יש 1,646 מקרים. 1,645 זהים. המקרה שנשאר רשום ב-ledger, עם הסיבה.
הריצות המוקדמות היו מועילות יותר. השניים לא הסכימו. מאז ירדו מה-ledger 12 שורות, עם כל תיקון שנכנס. שלוש דוגמאות.
אותו מספר שלם, שלוש צורות כתיבה.1785000000, 1785000000.0 ו-1.785e9 הם מספר JSON אחד. בצורה הקנונית שלושתם נכתבים אותו דבר, ולכן יש להם חתימה אחת. ה-schema קיבלה את שלושתם כמספר שלם. בדיקת ה-freshness ב-verifier של Python בדקה את ה-type, ראתה float בצורה השנייה והשלישית, ודחתה רשומה שנחתמה נכון. verifier ב-JavaScript בכלל לא מבחין בין הצורות: ה-parser שלו מחזיר מספר אחד. שני מימושים, אותם בייטים חתומים, תוצאות שונות, ולהבדל לא היה שום קשר לחתימה. במפרט יש עכשיו פסקה: מספר שלם נקבע לפי הערך, לא לפי צורת הכתיבה.
ארבעה ביטים עודפים. חתימה של 64 בייטים ב-base64url בלי padding תופסת 86 תווים. אלה 516 ביטים בשביל 512 ביטים של חתימה. התו האחרון מכיל ארבעה ביטים ששום אלגוריתם חתימה לא מייצר. decoder קפדני דורש שהם יהיו אפס. decoder סלחני מתעלם מהם. לכן את אותם 64 בייטים אפשר לכתוב ביותר מדרך אחת, וייתכן ששני verifiers לא יסכימו אם הצורות האלה הן אותה רשומה. התיקון הוא משפט אחד במפרט ו-pattern אחד ב-schema: התו האחרון חייב להיות A, Q, g או w. באותו pull request היה גם סקריפט שעובר על חתימות שפורסמו וסופר כמה מהן יפסיקו לעבור עם הכלל החדש.
סדר הבדיקות. ה-reference מפענח את החתימה לפני שהוא בודק את ה-schema. ה-verifier שלי עשה את ההפך. רשומה שגויה בשני אופנים נכשלת בכל verifier מסיבה אחרת. אף אחד מהסדרים לא שגוי. שורה אחת ב-ledger נסגרה כשהעברתי את הפענוח למקום שבו הוא נמצא ב-reference.
אף אחד מהם לא היה פגם קריפטוגרפי. Ed25519 היה בסדר לכל אורך הדרך. שלושתם יושבים בשכבה שמתחת לחתימה: מספרים, קידודים, סדר. זו השכבה שבה תיאור הפורמט הוא בדרך כלל משפט אחד, ושבה מימוש אחד מחליט בשקט בשביל כולם.
3. בשביל מה צריך vectors
verifier שני צריך משהו להישען עליו. זה סט ה-vectors: קלטים קבועים עם תוצאות צפויות, שמתפרסמים ליד הפורמט.
סט טוב מורכב בעיקר ממקרים שליליים. רשומה תקינה מוכיחה מעט. המקרים החשובים הם אלה שחייבים להיכשל, כל אחד מסיבה אחת בדיוק:
תוכן ששונה כשהחתימה נשארה כמו שהיא
שדה ששונה בגוף החתום
קישור שבור לרשומה הקודמת
מפתח ציבורי שגוי
שרשרת שבה שתי רשומות החליפו מקום
לכל vector יש תוצאה צפויה ו-reason code. verifier שדוחה את הרשומה שצריך לדחות, אבל מסיבה אחרת, לא עבר.
ואז כלל אחד הופך את הסט למחייב: ה-vectors קשורים למחרוזת גרסת ה-schema. אם הפורמט משתנה והמחרוזת לא, ה-CI נכשל. בלי הכלל הזה vectors הם דוגמאות. איתו הם ה-reference.
4. הפרויקט שלי נכשל במבחן הזה
Brand impersonation detector הוא self-hosted pipeline שאני מתחזק. כל החלטה על מועמד נרשמת כ-receipt חתום, וה-receipts יוצרים שרשרת. ב-README כתוב שכל מי שיש לו את המפתח הציבורי יכול לאמת אותם offline.
זה נכון, וזה המצב שתיארתי בחלק 1. יש verifier אחד. הוא יושב בחבילה שכותבת את ה-receipts.
יש על זה שני issues פתוחים.
Issue #4 מבקש תיקיית vectors/: receipt תקין, שרשרת תקינה, בייטים של asset ששונו, שדה manifest ששונה, חוליה שבורה בשרשרת, מפתח שגוי, שרשרת שסודרה מחדש. לכל אחד מצורפים ה-assets השמורים, המפתח הציבורי ו-expected.json. וגם verifier שני, שנכתב בלי לייבא את החבילה ורץ על ה-vectors ב-CI. מספיק סקריפט של קובץ אחד בשפה אחרת. המשימה גמורה כששני ה-verifiers מסכימים על כל vector, וכששינוי פורמט בלי מחרוזת schema חדשה צובע את ה-CI באדום.
Issue #5 קטן יותר ופחות נעים. את ראש השרשרת אפשר להחתים בחותמת זמן מרשות RFC 3161, כדי שאפשר יהיה לזהות שרשרת קטועה. הקוד בודק שה-token קשור בדיוק לבייטים שנחתמו. את שרשרת התעודות של הרשות הוא לא מאמת. token עם מבנה תקין מתקבל מכל signer. מסמך הפורמט אומר את זה במילים פשוטות, וגם ה-docstring של המודול. ה-issue מציע trust store ושלוש תוצאות נפרדות: אין token, token קשור וה-signer לא אומת, token קשור וה-signer אומת. בדיקת revocation לא נכללת, וגם לא שום אמירה לגבי המשקל המשפטי של חותמת זמן.
שני ה-issues פתוחים.
5. מה אני לוקח מזה
חתימה אומרת שהבייטים לא השתנו. היא לא אומרת ששני אנשים מתכוונים לאותו דבר כשהם אומרים "לאמת".
בשביל זה צריך קלטים קבועים, תוצאות שנכתבו מראש וקורא שני שהיה לו רק המסמך. כשהקורא השני לא מסכים, המסמך לא היה ברור. לתקן את המשפט, להוסיף vector, להריץ שוב.
זה איטי יותר מלהוציא verifier אחד. אני הוצאתי אחד.