בקרת גישה

אחת המטרות של Tink היא למנוע שיטות עבודה פסולות. קטע זה מעניין במיוחד את שתי נקודות:

  1. ספריית Tink מעודדת את השימוש כך שמשתמשים לא יכולים לגשת לחומר מפתח סודי. במקום זאת, צריך לאחסן מפתחות סודיים ב-KMS כשאפשר, באחת מהדרכים המוגדרות מראש שבהן Tink תומכת במערכות כאלה.
  2. ספריית Tink מונעת ממשתמשים לגשת לחלקים של המפתחות, כי לרוב הפעולה הזו גורמת לבאגי תאימות.

בפועל, לפעמים יש להפר את שני העקרונות האלה. לשם כך, Tink מספקת מנגנונים שונים.

אסימוני גישה למפתחות סודיים

כדי לקבל גישה לחומר מפתח סודי, למשתמשים צריך להיות אסימון (בדרך כלל זהו אובייקט של מחלקה מסוימת, ללא כל פונקציונליות). האסימון מסופק בדרך כלל בשיטה כמו InsecureSecretKeyAccess.get(). ב-Google, המשתמשים לא יכולים להשתמש בפונקציה הזו באמצעות Bazel BUILDVisibility. מחוץ ל-Google, בודקי האבטחה יכולים לחפש ב-codebase שלהם שימושים בפונקציה הזו.

תכונה שימושית אחת של אסימונים אלה היא שניתן להעביר אותם הלאה. לדוגמה, נניח שיש לכם פונקציה שמסדרת מפתח Tink שרירותי:

String serializeKey(Key key, @Nullable SecretKeyAccess secretKeyAccess);

למפתחות שיש בהם חומר מפתח סודי, הפונקציה הזו דורשת שהאובייקט secretKeyAccess יהיה לא null וישמור אסימון SecretKeyAccess בפועל. למפתחות שאין להם חומר סודי, המערכת תתעלם מ-secretKeyAccess.

בהינתן פונקציה כזו, אפשר לכתוב פונקציה שמסדרת קבוצת מפתחות שלמה: מחרוזת עם SerializeKeyset(KeysetHandle keyset, @Nullable SecretKeyAccesssecretKeyAccess);

הפונקציה הזו מפעילה את serializeKey לכל מפתח בקבוצת המפתחות באופן פנימי, ומעבירה את הערך הנתון secretKeyAccess לפונקציה הבסיסית. לאחר מכן, משתמשים שמתקשרים ל-serializeKeyset בלי שיצטרכו לערוך סידורי של חומר מפתח סודי יכולים להשתמש ב-null כארגומנט השני. משתמשים שצריכים לערוך חומר מפתח סודי בסדרה צריכים להשתמש ב-InsecureSecretKeyAccess.get().

גישה לחלקים של מפתח

באג אבטחה נפוץ יחסית הוא "התקפת שימוש חוזר במפתח". זה יכול לקרות כשמשתמשים עושים שימוש חוזר במפתח RSA, למשל המודול n והמעריכים d ו-e, בשתי הגדרות שונות (למשל, לחישוב חתימות והצפנות)1.

עוד טעות נפוצה יחסית כשמדובר במפתחות קריפטוגרפיים היא לציין חלק מהמפתח ולאחר מכן "להניח" שהמטא-נתונים שלו. לדוגמה, נניח שמשתמש רוצה לייצא מפתח ציבורי מסוג RSASSA-PSS מ-Tink, לשימוש בספרייה אחרת. ב-Tink, המפתחות האלה כוללים את החלקים הבאים:

  • המודול n
  • המעריך הציבורי e
  • המפרט של שתי פונקציות הגיבוב (hash) שנעשה בהן שימוש פנימי
  • אורך המלח שנעשה בו שימוש פנימי באלגוריתם.

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

עם זאת, התעלמות מפונקציות הגיבוב עלולה להיות טעות יקרה, כדי לראות זאת, נניח שבהמשך יתווסף מפתח חדש עם פונקציית גיבוב (hash) שונה לערכת המפתחות Tink. נניח לאחר מכן, שהמפתח מיוצא באמצעות השיטה, ומוענק לשותף עסקי, שמשתמש בו עם הספרייה השנייה. ספריית Tink מניחה עכשיו פונקציית גיבוב פנימית אחרת, ולא יכולה לאמת את החתימה.

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

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

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

שיטה מומלצת: משתמשים באובייקטים מסוג Tink מוקדם ככל האפשר בייבוא מפתחות

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

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

לדוגמה:

שימוש שאינו הקלדה:

void verifyEcdsaSignature(ECPoint ecPoint, byte[] signature, byte[] message)
        throws Exception {
    EcdsaParameters parameters =
        EcdsaParameters.builder()
            .setSignatureEncoding(EcdsaParameters.SignatureEncoding.IEEE_P1363)
            .setCurveType(EcdsaParameters.CurveType.NIST_P256)
            .setHashType(EcdsaParameters.HashType.SHA256)
            .setVariant(EcdsaParameters.Variant.NO_PREFIX)
            .build();
    EcdsaPublicKey key =
        EcdsaPublicKey.builder()
            .setParameters(parameters)
            .setPublicPoint(ecPoint)
            .build();
    KeysetHandle handle = KeysetHandle.newBuilder()
       .addEntry(KeysetHandle.importKey(key).withFixedId(1).makePrimary())
       .build();
    PublicKeyVerify publicKeyVerify = handle.getPrimitive(PublicKeyVerify.class);
    publicKeyVerify.verify(signature, message);
}

יש מצבים כאלה: באתר השיחה קל מאוד לשכוח שאסור להשתמש באותו ecPoint עם אלגוריתם אחר. לדוגמה, אם קיימת פונקציה דומה שנקראת encryptWithECHybridEncrypt, מבצע הקריאה החוזרת עשוי להשתמש באותה נקודת עקומה כדי להצפין הודעה. כתוצאה מכך, יכולה להיות בקלות נקודת חולשה.

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

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

השיפורים לא בוצעו, אבל עדיף לעבור באובייקט PublicKeyVerify. מספיק להעביר את הפונקציה הזו, ולכן העברת האובייקט PublicKeyVerify עשויה להגדיל את מספר המקומות שבהם אפשר להשתמש בפונקציה הזו. עם זאת, בשלב זה, הפונקציה הופכת לטריוויאלית למדי וניתן להצמיד אותה לכנס.

המלצה: כשקוראים חומר מפתח מהדיסק או מהרשת בפעם הראשונה, כדאי ליצור את אובייקטי ה-Tink המתאימים בהקדם האפשרי.

שימוש מוקלד:

KeysetHandle readEcdsaKeyFromFile(Path fileWithEcdsaKey) throws Exception {
    byte[] content = Files.readAllBytes(fileWithEcdsaKey);
    BigInteger x = new BigInteger(1, Arrays.copyOfRange(content, 0, 32));
    BigInteger y = new BigInteger(1, Arrays.copyOfRange(content, 32, 64));
    ECPoint point = new ECPoint(x, y);
    EcdsaParameters parameters =
        EcdsaParameters.builder()
            .setSignatureEncoding(EcdsaParameters.SignatureEncoding.IEEE_P1363)
            .setCurveType(EcdsaParameters.CurveType.NIST_P256)
            .setHashType(EcdsaParameters.HashType.SHA256)
            .setVariant(EcdsaParameters.Variant.NO_PREFIX)
            .build();
    EcdsaPublicKey key =
        EcdsaPublicKey.builder()
            .setParameters(parameters)
            .setPublicPoint(ecPoint)
            .build();
    return KeysetHandle.newBuilder()
       .addEntry(KeysetHandle.importKey(key).withFixedId(1).makePrimary())
       .build();
}

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

שיטה מומלצת: אימות כל הפרמטרים בייצוא המפתחות

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

דרך לא טובה לייצא מפתח ציבורי:

/** Provide the key to our users which do not have Tink. */
byte[] exportTinkHpkeKey(HpkePublicKey key) {
    return key.getPublicKeyBytes().toByteArray();
}

זה בעייתי. לאחר קבלת המפתח, הצד השלישי שמשתמש בו מניח כמה פרמטרים לגבי הפרמטרים שלו: לדוגמה, הוא יוצא מנקודת הנחה שאלגוריתם HPKE AEAD שבו נעשה שימוש למפתח 256-bit הזה היה AES-GCM וכן הלאה.

המלצה: כדאי לוודא שהפרמטרים תואמים לציפיות שלכם בייצוא המפתח.

דרך טובה יותר לייצא מפתח ציבורי:

/** Provide the key to our users which do not have Tink. */
byte[] exportTinkHpkeKeyForOurUsers(HpkePublicKey key) {
    // Our users assume we use KEM_P256_HKDF_SHA256 for the KEM.
    if (!key.getParameters().getKemId().equals(HpkeParameters.KemId.KEM_P256_HKDF_SHA256)) {
        throw new IllegalArgumentException("Bad parameters");
    }
    // Our users assume we use HKDF SHA256 to create the key material.
    if (!key.getParameters().getKdfId().equals(HpkeParameters.KdfId.HKDF_SHA256)) {
        throw new IllegalArgumentException("Bad parameters");
    }
    // Our users assume that we use AES GCM with 256 bit keys.
    if (!key.getParameters().getAeadId().equals(HpkeParameters.AeadId.AES_256_GCM)) {
        throw new IllegalArgumentException("Bad parameters");
    }
    // Our users assume we follow the standard and do not add a Tink style prefix
    if (!key.getParameters().getVariant().equals(HpkeParameters.Variant.NO_PREFIX)) {
        throw new IllegalArgumentException("Bad parameters");
    }
    return key.getPublicKeyBytes().toByteArray();
}