סכמת חתימת APK גרסה 2

סכמת החתימה של APK בגרסה 2 היא סכמת חתימה של קבצים מלאים, שמגבירה את מהירות האימות ומחזקת את האחריות על התקינות על ידי זיהוי שינויים בחלקים המוגנים של ה-APK.

לאחר חתימה באמצעות סכימת החתימה של APK בגרסה 2, היא מוסיפה בלוק חתימת APK לקובץ ה-APK מיד לפני הקטע ZIP Central Directory. בתוך הבלוק לחתימה על קובץ ה-APK, חתימות v2 ומידע על זהות החתום מאוחסנים בבלוק של סכמת חתימה על קובץ APK v2.

קובץ APK לפני ואחרי החתימה

איור 1. קובץ APK לפני ואחרי החתימה

סכמת החתימה על APKs v2 הוצגה ב-Android 7.0 (Nougat). כדי שאפשר יהיה להתקין קובץ APK במכשירי Android 6.0 (Marshmallow) ובמכשירים ישנים יותר, צריך לחתום על קובץ ה-APK באמצעות חתימה על JAR לפני החתימה עליו באמצעות הסכימה v2.

בלוק לחתימה על קובץ APK

כדי לשמור על תאימות לאחור לפורמט APK v1, חתימות APK בגרסה 2 ואילך מאוחסנות בתוך בלוק חתימת APK – קונטיינר חדש שהוצג כדי לתמוך בסכמת החתימה APK v2. בקובץ APK, הבלוק לחתימה על קובץ ה-APK נמצא מיד לפני הספרייה המרכזית של קובץ ה-ZIP, שנמצאת בסוף הקובץ.

הבלוק מכיל צמדים של מזהה-ערך שארוזים באופן שמקל על איתור הבלוק ב-APK. החתימה של גרסה 2 של ה-APK מאוחסנת כצמד מזהה-ערך עם המזהה 0x7109871a.

פורמט

הפורמט של בלוק החתימה של קובץ ה-APK הוא: (כל השדות המספריים הם בסדר little-endian):

  • size of block בבייטים (לא כולל השדה הזה) (uint64)
  • רצף של זוגות מזהה-ערך עם קידומת באורך uint64:
    • ID (uint32)
    • value (אורך משתנה: אורך הצמד פחות 4 בייטים)
  • size of block בבייטים – כמו בשדה הראשון (uint64)
  • magic "APK Sig Block 42" (16 בייטים)

כדי לנתח את קובץ ה-APK, קודם מאתרים את ההתחלה של הספרייה המרכזית של קובץ ה-ZIP (על ידי חיפוש הרשומה ZIP End of Central Directory בסוף הקובץ, ואז קריאת הזזת ההתחלה של הספרייה המרכזית מהרשומה). הערך magic מספק דרך מהירה לקבוע שהקטע שלפני Central Directory הוא ככל הנראה בלוק החתימה של קובץ ה-APK. לאחר מכן, הערך size of block מפנה ביעילות לתחילת הבלוק בקובץ.

כשמתרגמים את הבלוק, צריך להתעלם מזוגות מזהה-ערך עם מזהים לא ידועים.

חסימה של סכמת חתימה על APK v2

ה-APK חתום על ידי חותם/זהות אחד או יותר, שכל אחד מהם מיוצג על ידי מפתח חתימה. המידע הזה מאוחסן כבלוק של APK Signature Scheme v2. לגבי כל חתום, נשמרים הפרטים הבאים:

  • צמדי ערכים (signature algorithm, ‏ digest, ‏ signature). התקציר מאוחסן לצורך אימות חתימה כפול בגלל בדיקת התקינות של תוכן ה-APK.
  • שרשרת אישורים X.509 שמייצגת את זהות החותם.
  • מאפיינים נוספים כצמדי מפתח/ערך.

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

פורמט

הבלוק של סכמת החתימה על APK בגרסה 2 מאוחסן בתוך הבלוק של חתימת ה-APK במזהה 0x7109871a.

הפורמט של הבלוק של סכמת החתימה על APK v2 הוא כדלקמן (כל הערכים המספריים הם little-endian, וכל השדות עם קידומת אורך משתמשים ב-uint32 לאורכו):

  • רצף עם קידומת אורך של signer עם קידומת אורך:
    • signed data עם קידומת באורך :
      • רצף עם קידומת אורך של digests עם קידומת אורך:
      • רצף עם קידומת באורך של X.509 certificates:
        • X.509 certificate עם קידומת באורך (ASN.1 DER form)
      • רצף עם קידומת אורך של additional attributes עם קידומת אורך:
        • ID (uint32)
        • value (אורך משתנה: אורך המאפיין הנוסף – 4 בייטים)
    • רצף עם קידומת אורך של signatures עם קידומת אורך:
      • signature algorithm ID (uint32)
      • signature עם קידומת באורך מעל signed data
    • public key עם קידומת באורך (SubjectPublicKeyInfo, טופס ASN.1 DER)

מזהי אלגוריתמים של חתימות

  • 0x0101 – RSASSA-PSS עם סיכום SHA2-256, ‏ SHA2-256 MGF1, ‏ 32 בייטים של מלח, כותרת נלווית: 0xbc
  • 0x0102—RSASSA-PSS עם תקציר SHA2-512, SHA2-512 MGF1, 64 בייטים של salt, נגרר: 0xbc
  • 0x0103 – RSASSA-PKCS1-v1_5 עם סיכום SHA2-256. האפשרות הזו מיועדת למערכות build שדורשות חתימות גורליות.
  • 0x0104 – RSASSA-PKCS1-v1_5 עם סיכום SHA2-512. האפשרות הזו מיועדת למערכות build שדורשות חתימות גורליות.
  • 0x0201 – ECDSA עם סיכום SHA2-256
  • 0x0202 – ECDSA עם סיכום SHA2-512
  • 0x0301 – מודעות דינמיות לרשת החיפוש עם סיכום SHA2-256

פלטפורמת Android תומכת בכל אלגוריתמי החתימה שלמעלה. כלים לחתימה יכולים לתמוך בקבוצת משנה של האלגוריתמים.

הגדלים והעקומות של מפתחות EC נתמכים:

  • RSA: ‏ 1024, ‏ 2048, ‏ 4096, ‏ 8192, ‏ 16384
  • EC: ‏ NIST P-256, ‏ P-384, ‏ P-521
  • חוק ה-DSA: 1024, 2048, 3072

תוכן שמוגן על ידי הגנה על תקינות האפליקציה

כדי להגן על תוכן של APK, חבילת APK מורכבת מארבעה קטעים:

  1. התוכן של רשומות ZIP (מ-offset 0 ועד לתחילת הבלוק של חתימה על קובץ APK)
  2. בלוק לחתימה על קובץ APK
  3. ספריית ZIP Central
  4. ZIP End of Central Directory

קטעי APK אחרי החתימה

איור 2. קטעי APK אחרי החתימה

סכמת החתימה על APKs v2 מגינה על השלמות של קטעים 1, 3, 4 ואת הבלוק signed data של סכמת החתימה על APKs v2 שנמצא בתוך קטע 2.

תקינות החלקים 1, 3 ו-4 מוגנת על ידי סיכום אחד או יותר של התוכן שלהם, שנשמר בבלוק signed data, שמוגן בתורו על ידי חתימה אחת או יותר.

התקציר של סעיפים 1, 3 ו-4 מחושב באופן הבא, בדומה לעץ מרקל דו-שלבי. כל קטע מחולק למקטעים רצופים של 1MB‏ (220 בייטים). יכול להיות שהקטע האחרון בכל קטע יהיה קצר יותר. הדיגסט של כל מקטע מחושב על סמך שרשור של הבייט 0xa5, אורך המקטע בבייטים (uint32 ב-little-endian) ותוכן המקטע. הסיכום של הרמה העליונה מחושב על סמך שרשור של הבית 0x5a, מספר הקטעים (uint32 ב-little-endian) ושרשור של סיכומי הקטעים בסדר שבו הקטעים מופיעים ב-APK. התקציר מחושב במקטעים כדי לאפשר האצת החישוב על ידי מקבילה שלו.

סיכום APK

איור 3. סיכום APK

ההגנה על הקטע 4 (ZIP End of Central Directory) מורכבת בגלל הקטע שמכיל את ההיסט של ZIP Central Directory. ההיסט משתנה כשהגודל של בלוק החתימה של ה-APK משתנה, למשל כשמוסיפים חתימה חדשה. לכן, כשמחשבים את הסיכום על סמך ZIP End of Central Directory, צריך להתייחס לשדה שמכיל את ה-offset של ZIP Central Directory כאל שדה שמכיל את ה-offset של בלוק החתימה של ה-APK.

אמצעי הגנה מפני רולבק

תוקף יכול לנסות לאמת קובץ APK בחתימה v2 כקובץ APK בחתימה v1 בפלטפורמות Android שתומכות באימות של קובץ APK בחתימה v2. כדי לצמצם את ההתקפה הזו, חבילות APK בחתימה v2 שגם חתומות בחתימה v1 חייבות לכלול את המאפיין X-Android-APK-Signed בקטע הראשי של קובצי META-INF/*.SF שלהן. הערך של המאפיין הוא קבוצה של מזהים של סכימת חתימה של APK שמופרדים בפסיקים (המזהה של סכמת הזו הוא 2). כשמאמתים את החתימה של v1, מאמת ה-APK נדרש לדחות חבילות APK שאין להן חתימה של סכמת החתימה של ה-APK שמאמת ה-APK מעדיף מתוך הקבוצה הזו (למשל, סכימה v2). ההגנה הזו מסתמכת על העובדה שקובצי Meta-INF/*.SF מוגנים באמצעות חתימות v1. ראו את הקטע אימות של APK בחתימה JAR.

תוקף יכול לנסות להסיר חתימות חזקות יותר מהבלוק של סכמת החתימה על APK v2. כדי לצמצם את התקפה הזו, רשימת המזהים של אלגוריתמי החתימה שבהם חתמתם על קובץ ה-APK מאוחסנת בבלוק signed data שמוגן על ידי כל חתימה.

אימות

ב-Android 7.0 ואילך, אפשר לאמת קובצי APK לפי הסכימה לחתימה על קובצי APK מגרסה 2 ואילך או לפי חתימת JAR (הסכימה מגרסה 1). בפלטפורמות ישנות יותר מתעלמים מחתימות v2 ומאמתים רק חתימות v1.

תהליך אימות החתימה של קובץ ה-APK

איור 4. תהליך האימות של חתימת ה-APK (השלבים החדשים מסומנים באדום)

אימות של סכמת חתימה על APK v2

  1. מאתרים את בלוק חתימת ה-APK ומוודאים:
    1. שני שדות גודל של בלוק החתימה של קובץ ה-APK מכילים את אותו ערך.
    2. אחרי הרשומה ZIP Central Directory מופיעה הרשומה ZIP End of Central Directory.
    3. אחרי ZIP End of Central Directory לא מופיעים נתונים נוספים.
  2. מאתרים את הבלוק הראשון של סכמת החתימה על APK v2 בתוך הבלוק של חתימת ה-APK. אם הבלוק V2 קיים, ממשיכים לשלב 3. אחרת, עוברים לאימות ה-APK באמצעות סכמה v1.
  3. לכל signer בבלוק חתימת ה-APK גרסה 2:
    1. צריך לבחור את signature algorithm ID הכי נתמך מ-signatures. סדר החוזק נקבע לפי כל גרסה של הטמעה או פלטפורמה.
    2. מוודאים שה-signature התואם מ-signatures תואם ל-signed data באמצעות public key. (עכשיו אפשר לנתח את signed data).
    3. מוודאים שהרשימה הממוזערת של מזהי אלגוריתמי החתימה ב-digests וב-signatures זהה. (זהו כדי למנוע הסרה/הוספה של חתימות).
    4. חישוב התקציר של תוכן ה-APK באמצעות אותו אלגוריתם תקציר שבו משתמש אלגוריתם התקציר.
    5. מוודאים שה-digest המחושב זהה ל-digest התואם מ-digests.
    6. מוודאים ש-SubjectPublicKeyInfo של certificate הראשון ב-certificates זהה ל-public key.
  4. האימות יצליח אם נמצא לפחות signer אחד ושלב 3 הצליח לכל signer שנמצא.

הערה: אם מתרחשת כשל בשלב 3 או 4, אסור לאמת את ה-APK באמצעות הסכימה v1.

אימות APK בחתימת JAR (סכמת v1)

קובץ ה-APK החתום ב-JAR הוא קובץ JAR חתום רגיל, שחייב להכיל בדיוק את הרשומות שמפורטות בקובץ META-INF/MANIFEST.MF, וכל הרשומות חייבות להיות חתומות על ידי אותה קבוצה של חותמים. התקינות שלו מאומתת באופן הבא:

  1. כל חותם מיוצג באמצעות רשומת JAR /<signer>.SF ו-meta-INF/<signer>.(RSA|DSA|EC).
  2. <signer>.(RSA|DSA|EC) הוא קובץ PKCS #7 CMS ContentInfo עם מבנה SignedData שהחתימה שלו מאומתת באמצעות הקובץ <signer>.SF.
  3. קובץ <signer>.SF מכיל תקציר של כל הקובץ של Meta-INF/MANIFEST.MF ותקצירים של כל קטע ב-meta-INF/MANIFEST.MF. מתבצע אימות של הסיכום של כל הקובץ של MANIFEST.MF. אם האימות הזה נכשל, מתבצע אימות של הסיכום של כל קטע ב-MANIFEST.MF.
  4. עבור כל רשומת JAR שמוגנת על ידי בדיקת תקינות, הקובץ META-INF/MANIFEST.MF מכיל קטע בעל שם תואם שמכיל את הסיכום של התוכן הלא דחוס של הרשומה. כל התקצירים האלה מאומתים.
  5. אימות ה-APK נכשל אם חבילת ה-APK מכילה רשומות JAR שלא מופיעות ב-MANIFEST.MF ולא חלק מחתימה JAR.

לכן, שרשרת ההגנה היא <signer>.(RSA|DSA|EC) -> <signer>.SF -> MANIFEST.MF -> התוכן של כל רשומת JAR שמוגנת על ידי בדיקת תקינות.