ארכיטקטורה של חתימה במכשיר

החל מגרסה 12 של Android, המודול Android Runtime‏ (ART) הוא מודול Mainline. יכול להיות שעדכון המודול ידרוש בנייה מחדש של ארטיפקטים של הידור מראש (AOT) של קובצי jar ב-bootclasspath ושל שרת המערכת. מכיוון שהפריטים האלה רגישים לאבטחה, ב-Android 12 נעשה שימוש בתכונה שנקראת חתימה במכשיר כדי למנוע פגיעה בפריטים האלה. בדף הזה מוסבר על הארכיטקטורה של החתימה במכשיר ועל האינטראקציות שלה עם תכונות אבטחה אחרות של Android.

העיצוב הכללי

החתימה במכשיר כוללת שני רכיבים מרכזיים:

  • odrefresh הוא חלק מהמודול הראשי של ART. הוא אחראי ליצירת פריטי המידע שנוצרים בסביבת זמן הריצה. הוא בודק את הארטיפקטים הקיימים בהשוואה לגרסה המותקנת של מודול ה-ART, קובצי ה-jar של נתיב הטעינה (bootclasspath) וקובצי ה-jar של שרת המערכת כדי לקבוע אם הם מעודכנים או שצריך ליצור אותם מחדש. אם צריך ליצור אותם מחדש, odrefresh יוצר אותם ומאחסן אותם.

  • odsign הוא קובץ בינארי שנכלל בפלטפורמת Android. הוא פועל במהלך ההפעלה המוקדמת, מיד אחרי שמחייבים את המחיצה /data. התפקיד העיקרי שלו הוא להפעיל את odrefresh כדי לבדוק אם צריך ליצור או לעדכן ארטיפקטים. לכל ארטיפקט חדש או מעודכן שנוצר על ידי odrefresh, odsign מחשב פונקציית גיבוב. התוצאה של חישוב גיבוב כזה נקראת סיכום קובץ. לגבי כל ארטיפקטים שכבר קיימים, odsign מאמתת שהדיגסטים של הארטיפקטים הקיימים תואמים לדיגסטים ש-odsign חישבה בעבר. כך אפשר לוודא שהארטיפקטים לא נפרצו.

בתנאים של שגיאה, למשל כשהסכום המאוחזר של קובץ לא תואם, odrefresh ו-odsign מוחקים את כל הארטיפקטים הקיימים ב-/data ומנסים ליצור אותם מחדש. אם הבדיקה תיכשל, המערכת תעבור למצב JIT.

odrefresh ו-odsign מוגנים על ידי dm-verity, והם חלק משרשרת ההפעלה המאומתת של Android.

חישוב של סיכומי קבצים באמצעות fs-verity

fs-verity היא תכונה של ליבה של Linux שמבצעת אימות של נתוני קובץ על סמך עץ Merkle. הפעלת fs-verity בקובץ גורמת למערכת הקבצים ליצור עץ Merkle מעל נתוני הקובץ באמצעות גיבוב SHA-256, לאחסן אותו במיקום מוסתר לצד הקובץ ולסמן את הקובץ כקובץ לקריאה בלבד. fs-verity מאמת באופן אוטומטי את נתוני הקובץ מול עץ Merkle על פי דרישה בזמן הקריאה. fs-verity הופך את גיבוב הבסיס של עץ Merkle לזמין כערך שנקרא סיכום הקובץ של fs-verity, ומוודא שכל הנתונים שנקראו מהקובץ תואמים לסיכום הקובץ הזה.

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

במכשירים שהליבתם לא תומכת ב-fs-verity, odsign חוזר לחישוב של סיכומי קבצים במרחב המשתמש. odsign משתמש באותו אלגוריתם גיבוב שמבוסס על עץ Merkle כמו fs-verity, כך שהסיכומים (digests) זהים בשני המקרים. ‏fs-verity נדרש בכל המכשירים שהושקו עם Android מגרסה 11 ואילך.

אחסון של סיכומי הקבצים

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

אימות של סיכומי קבצים

בכל הפעלה, אם odrefresh קובע שהארטיפקטים הקיימים מעודכנים, odsign מוודא שהקבצים לא נפרצו מאז שנוצרו. odsign עושה זאת על ידי אימות סיכומי הקבצים. קודם כול, הוא מאמת את החתימה של odsign.info. אם החתימה תקינה, המערכת ב-odsign מאמתת שה-digest של כל קובץ תואם ל-digest התואם ב-odsign.info.

מפתחות חתימה מהימנים

ב-Android 12 נוספה תכונה חדשה ל-Keystore שנקראת 'מפתחות של שלב האתחול', שמטפלת בבעיות האבטחה הבאות:

  • מה מונע מתוקף להשתמש במפתח החתימה שלנו כדי לחתום על הגרסה שלו ל-odsign.info?
  • מה מונע מתוקף ליצור מפתח חתימה משלו ולהשתמש בו כדי לחתום על גרסה משלו של odsign.info?

מפתחות של שלבי אתחול מפצלים את מחזור האתחול של Android לרמות, ומקשרים באופן קריפטוגרפי את היצירה והשימוש במפתח לרמה מסוימת. odsign יוצר את מפתח החתימה שלו בשלב מוקדם, כשרק קוד מהימן פועל ומוגן באמצעות dm-verity.

הרמות של שלבי האתחול ממוספרות מ-0 עד המספר הקסום 1000000000. במהלך תהליך האתחול של Android, אפשר להגדיל את רמת האתחול על ידי הגדרת מאפיין מערכת מ-init.rc. לדוגמה, הקוד הבא מגדיר את רמת האתחול ל-10:

setprop keystore.boot_level 10

לקוחות של Keystore יכולים ליצור מפתחות שמקושרים לרמת אתחול מסוימת. לדוגמה, אם יוצרים מפתח לרמת אתחול 10, אפשר להשתמש במפתח הזה רק כשהמכשיר נמצא ברמת אתחול 10.

odsign משתמש ברמת האתחול 30, ומפתח החתימה שהוא יוצר קשור לרמת האתחול הזו. לפני שמשתמשים במפתח כדי לחתום על ארטיפקטים, odsign מאמת שהמפתח קשור לרמת האתחול 30.

כך אפשר למנוע את שתי ההתקפות שתיארנו קודם בסעיף הזה:

  • תוקפים לא יכולים להשתמש במפתח שנוצר, כי עד שהתוקף יכול להריץ קוד זדוני, רמת האתחול עולה מעל 30 ו-Keystore דוחה פעולות שמשתמשות במפתח.
  • תוקפים לא יכולים ליצור מפתח חדש, כי עד שהתוקף יכול להריץ קוד זדוני, רמת האתחול עולה מעל 30, ו-Keystore מסרב ליצור מפתח חדש ברמת האתחול הזו. אם תוקף יוצר מפתח חדש שלא קשור לרמת האתחול 30, odsign דוחה אותו.

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

הטמעה של Keymaster 4.0

גרסאות שונות של Keymaster מטפלות בהטמעה של מפתחות של שלב האתחול באופן שונה. במכשירים עם TEE/Strongbox של Keymaster 4.0, ההטמעה מתבצעת על ידי Keymaster באופן הבא:

  1. בהפעלה הראשונה, Keystore יוצר מפתח סימטרי K0 עם התג MAX_USES_PER_BOOT שמוגדר כ-1. כלומר, אפשר להשתמש במפתח רק פעם אחת בכל הפעלה.
  2. במהלך האתחול, אם רמת האתחול עולה, אפשר ליצור מפתח חדש לרמת האתחול הזו מ-K0 באמצעות פונקציית HKDF: ‏ Ki+i=HKDF(Ki, "some_fixed_string"). לדוגמה, אם עוברים מרמת אתחול 0 לרמת אתחול 10, קוראים ל-HKDF 10 פעמים כדי להפיק את K10 מ-K0.
  3. כשרמת האתחול משתנה, המפתח של רמת האתחול הקודמת נמחק מהזיכרון והמפתחות שמשויכים לרמות אתחול קודמות כבר לא זמינים.

    המפתח K0 הוא מפתח MAX_USES_PER_BOOT=1. המשמעות היא שאי אפשר להשתמש במפתח הזה גם בשלב מאוחר יותר של האתחול, כי תמיד מתרחש לפחות מעבר אחד ברמת האתחול (לרמת האתחול הסופית).

כשלקוח של Keystore כמו odsign מבקש ליצור מפתח ברמת האתחול i, ה-blob שלו מוצפן באמצעות המפתח Ki. מכיוון שהמפתח Ki לא זמין אחרי שלב האתחול i, אי אפשר ליצור או לפענח אותו בשלבים מאוחרים יותר של האתחול.

הטמעה של Keymaster 4.1 ו-KeyMint 1.0

ההטמעות של Keymaster 4.1 ו-KeyMint 1.0 דומות במידה רבה להטמעה של Keymaster 4.0. ההבדל העיקרי הוא ש-K0 הוא לא מפתח MAX_USES_PER_BOOT, אלא מפתח EARLY_BOOT_ONLY, שהוצג ב-Keymaster 4.1. אפשר להשתמש במפתח EARLY_BOOT_ONLY רק בשלבים המוקדמים של האתחול, כשלא פועל קוד לא מהימן. כך מתקבלת רמה נוספת של הגנה: בהטמעה של Keymaster 4.0, תוקף שמפר את מערכת הקבצים ו-SELinux יכול לשנות את מסד הנתונים של Keystore כדי ליצור מפתח MAX_USES_PER_BOOT=1 משלו לחתימה על ארטיפקטים. אי אפשר לבצע התקפה כזו בהטמעות של Keymaster 4.1 ו-KeyMint 1.0, כי אפשר ליצור מפתחות EARLY_BOOT_ONLY רק במהלך האתחול המוקדם.

הרכיב הציבורי של מפתחות חתימה מהימנים

odsign מאחזר את הרכיב של המפתח הציבורי של מפתח החתימה מ-Keystore. עם זאת, מערכת Keystore לא מאחזרת את המפתח הציבורי הזה מ-TEE/SE שמכיל את המפתח הפרטי התואם. במקום זאת, הוא מאחזר את המפתח הציבורי ממסד הנתונים שלו בדיסק. כלומר, תוקף שמפר את מערכת הקבצים יכול לשנות את מסד הנתונים של Keystore כך שיכיל מפתח ציבורי שנמצא בחלק מזווג מפתחות ציבורי/פרטי שנמצא בשליטתו.

כדי למנוע את ההתקפה הזו, odsign יוצר מפתח HMAC נוסף באותה רמת אתחול כמו מפתח החתימה. לאחר מכן, כשיוצרים את מפתח החתימה, odsign משתמש במפתח ה-HMAC הזה כדי ליצור חתימה של המפתח הציבורי ולאחסן אותה בדיסק. בהפעלות הבאות, כשמאחזרים את המפתח הציבורי של מפתח החתימה, משתמשים במפתח ה-HMAC כדי לוודא שהחתימה בדיסק תואמת לחתימה של המפתח הציבורי שאוחזר. אם הם תואמים, המפתח הציבורי מהימן, כי אפשר להשתמש במפתח ה-HMAC רק בשלבים מוקדמים של האתחול, ולכן תוקף לא יכול ליצור אותו.