Kontrol Akses

Salah satu tujuan Tink adalah mencegah praktik yang buruk. Dua hal yang menarik di bagian ini adalah:

  1. Tink mendorong penggunaan sedemikian rupa sehingga pengguna tidak dapat mengakses materi kunci rahasia. Sebagai gantinya, kunci rahasia harus disimpan di KMS jika memungkinkan menggunakan salah satu cara yang telah ditetapkan yang digunakan Tink untuk mendukung sistem tersebut.
  2. Tink mencegah pengguna mengakses bagian kunci, karena hal itu sering menyebabkan bug kompatibilitas.

Dalam praktiknya, tentu saja kedua prinsip ini terkadang harus dilanggar. Untuk keperluan ini, Tink menyediakan mekanisme yang berbeda.

Token Akses Kunci Rahasia

Untuk mengakses materi kunci rahasia, pengguna harus memiliki token (yang biasanya hanya merupakan objek dari beberapa class, tanpa fungsi apa pun). Token ini biasanya disediakan oleh metode seperti InsecureSecretKeyAccess.get(). Dalam Google, pengguna tidak dapat menggunakan fungsi ini menggunakan visibilitas Bazel BUILD. Di luar Google, peninjau keamanan dapat menelusuri codebase untuk penggunaan fungsi ini.

Salah satu fitur yang berguna dari token ini adalah dapat diteruskan. Misalnya, anggaplah Anda memiliki fungsi yang melakukan serialisasi kunci Tink arbitrer:

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

Untuk kunci yang memiliki materi kunci rahasia, fungsi ini memerlukan objek secretKeyAccess non-null dan menyimpan token SecretKeyAccess sebenarnya. Untuk kunci yang tidak memiliki materi rahasia, secretKeyAccess akan diabaikan.

Dengan fungsi tersebut, Anda dapat menulis fungsi yang menserialisasi seluruh keyset: String serializeKeyset(KeysetHandle keyset, @Nullable SecretKeyAccess secretKeyAccess);

Fungsi ini memanggil serializeKey untuk setiap kunci dalam keyset secara internal, dan meneruskan secretKeyAccess yang diberikan ke fungsi yang mendasarinya. Pengguna yang kemudian memanggil serializeKeyset tanpa perlu melakukan serialisasi materi kunci rahasia dapat menggunakan null sebagai argumen kedua. Pengguna yang perlu melakukan serialisasi materi kunci rahasia harus menggunakan InsecureSecretKeyAccess.get().

Akses bagian kunci

Bug keamanan yang relatif umum adalah "serangan penggunaan ulang kunci". Hal ini dapat terjadi saat pengguna menggunakan kembali, misalnya, modulus n, eksponen d, dan e dari kunci RSA, dalam dua setelan yang berbeda (misalnya untuk menghitung tanda tangan dan enkripsi)1.

Kesalahan lain yang relatif umum saat menangani kunci kriptografis adalah menentukan bagian kunci, lalu "mengasumsikan" metadatanya. Misalnya, pengguna ingin mengekspor kunci publik RSASSA-PSS dari Tink untuk digunakan dengan library lain. Di Tink, kunci ini memiliki bagian berikut:

  • Modulus n
  • Eksponen publik e
  • Spesifikasi dua fungsi hash yang digunakan secara internal
  • Panjang salt yang digunakan secara internal dalam algoritma.

Saat mengekspor kunci tersebut, Anda dapat mengabaikan fungsi hash dan panjang salt. Hal ini sering kali dapat berfungsi dengan baik, karena sering kali library lain tidak meminta fungsi hash (dan misalnya asumsikan SHA256 digunakan), dan fungsi hash yang digunakan di Tink secara kebetulan sama dengan di library lain (atau mungkin fungsi hash dipilih secara khusus sehingga berfungsi bersama library lainnya).

Meskipun demikian, mengabaikan fungsi {i>hash<i} akan menjadi kesalahan yang potensial. Untuk melihatnya, misalkan kunci baru dengan fungsi hash yang berbeda ditambahkan ke keyset Tink. Misalkan kemudian, kunci diekspor dengan metode, dan diberikan ke partner bisnis, yang menggunakannya dengan library lain. Tink sekarang mengasumsikan fungsi hash internal yang berbeda, dan tidak dapat memverifikasi tanda tangan.

Dalam hal ini, fungsi yang mengekspor kunci akan gagal jika fungsi hash tidak cocok dengan yang diharapkan oleh library lain: jika tidak, kunci yang diekspor tidak berguna karena membuat ciphertext atau tanda tangan yang tidak kompatibel.

Untuk mencegah kesalahan semacam itu, Tink membatasi fungsi yang memberikan akses ke materi kunci yang hanya sebagian, tetapi dapat disalahartikan sebagai kunci penuh. Misalnya, di Java Tink menggunakan RestrictedApi untuk ini.

Saat menggunakan anotasi tersebut, pengguna bertanggung jawab untuk mencegah serangan penggunaan ulang kunci dan inkompatibilitas.

Praktik Terbaik: Gunakan objek Tink sedini mungkin pada impor kunci

Anda paling sering menemukan metode yang dibatasi dengan "akses kunci parsial" saat mengekspor kunci dari atau mengimpor kunci ke Tink.

Hal ini meminimalkan risiko serangan kebingungan kunci, karena objek Tink Key sepenuhnya menentukan algoritme yang benar dan menyimpan semua metadata bersama dengan materi kunci.

Perhatikan contoh berikut:

Penggunaan non-jenis:

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);
}

Ini rentan terhadap error: di situs panggilan, sangat mudah untuk melupakan bahwa Anda tidak boleh menggunakan ecPoint yang sama dengan algoritme lain. Misalnya, jika ada fungsi serupa yang disebut encryptWithECHybridEncrypt, pemanggil dapat menggunakan titik kurva yang sama untuk mengenkripsi pesan, yang dapat dengan mudah menyebabkan kerentanan.

Sebagai gantinya, sebaiknya ubah verifyEcdsaSignature agar argumen pertama adalah EcdsaPublicKey. Bahkan, setiap kali kunci dibaca dari disk atau jaringan, kunci tersebut harus segera dikonversi menjadi objek EcdsaPublicKey: pada tahap ini, Anda sudah mengetahui cara kunci digunakan, jadi sebaiknya commit ke sana.

Kode sebelumnya dapat ditingkatkan lagi. Daripada meneruskan EcdsaPublicKey, sebaiknya teruskan KeysetHandle. Ini menyiapkan kode untuk rotasi kunci tanpa pekerjaan tambahan. Jadi, ini harus diutamakan.

Namun, peningkatan tidak dilakukan: akan lebih baik untuk meneruskan objek PublicKeyVerify: hal ini cukup untuk fungsi ini, sehingga meneruskan objek PublicKeyVerify berpotensi meningkatkan tempat fungsi ini dapat digunakan. Namun, pada titik ini, fungsinya menjadi agak kecil dan dapat disisipkan.

Rekomendasi: Saat materi kunci dibaca dari disk atau jaringan untuk pertama kalinya, buat objek Tink yang sesuai sesegera mungkin.

Penggunaan yang diketik:

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();
}

Dengan menggunakan kode tersebut, kita langsung mengonversi array byte menjadi objek Tink saat dibaca, dan menentukan sepenuhnya algoritme apa yang harus digunakan. Pendekatan ini meminimalkan kemungkinan terjadinya serangan {i>key kebingungan.<i}

Praktik Terbaik: Memverifikasi semua parameter pada ekspor kunci

Misalnya, jika Anda menulis fungsi yang mengekspor kunci publik HPKE:

Cara yang buruk untuk mengekspor kunci publik:

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

Ini bermasalah. Setelah menerima kunci, pihak ketiga yang menggunakannya akan membuat beberapa asumsi tentang parameter kunci tersebut: misalnya, algoritma HPKE AEAD yang digunakan untuk kunci 256-bit ini adalah AES-GCM, dan seterusnya.

Rekomendasi: Pastikan parameternya sama dengan yang Anda harapkan pada ekspor kunci.

Cara yang lebih baik untuk mengekspor kunci publik:

/** 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();
}