ייצוא חומר מפתח באופן פרוגרמטי

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

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

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

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

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

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

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

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

בעזרת פונקציה כזו, אפשר לכתוב פונקציה שממירה לסדרה (serialize) קבוצת מפתחות שלמה:

String serializeKeyset(KeysetHandle keyset, @Nullable SecretKeyAccess
secretKeyAccess);

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

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

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

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

כשממירים מפתח Tink לפורמט אחר, צריך לוודא שהמטא-נתונים של מפתח Tink תואמים למטא-נתונים (שמפורטים במרומז) של הפורמט האחר. אם הוא לא תואם, ההמרה חייבת להיכשל.

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

המשתמשים בממשקי ה-API האלה אחראים למנוע גם התקפות של שימוש חוזר במפתחות וגם אי-תאימות.

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

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

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

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

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

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

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

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

/** Provide the key to our users which don't 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 don't add a Tink style prefix
    if (!key.getParameters().getVariant().equals(HpkeParameters.Variant.NO_PREFIX)) {
        throw new IllegalArgumentException("Bad parameters");
    }
    return key.getPublicKeyBytes().toByteArray();
}

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

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

דוגמה:

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

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(RegistryConfiguration.get(), 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 באופן מיידי כשקוראים אותו, ומציינים במדויק באיזה אלגוריתם צריך להשתמש. הגישה הזו מפחיתה את הסבירות למתקפות עיקריות של בלבול.