Tink tidak menganjurkan praktik buruk yang terkait dengan kunci, seperti:
- Akses pengguna ke materi kunci rahasia – Sebagai gantinya, kunci rahasia harus disimpan di KMS jika memungkinkan menggunakan salah satu cara standar yang digunakan Tink untuk mendukung sistem tersebut.
- Akses pengguna ke bagian kunci – Tindakan ini sering kali menyebabkan bug kompatibilitas.
Pada kenyataannya, ada kasus yang mengharuskan pelanggaran terhadap prinsip-prinsip ini. Tink menyediakan mekanisme agar dapat melakukannya dengan aman yang dijelaskan di bagian berikut.
Token Akses Kunci Rahasia
Untuk mengakses materi kunci rahasia, pengguna harus memiliki token (yang
biasanya merupakan objek dari beberapa class, tanpa fungsi apa pun). Token tersebut
biasanya disediakan oleh metode seperti InsecureSecretKeyAccess.get()
. Di
Google, pengguna dicegah menggunakan fungsi ini menggunakan visibilitas
BUILD Bazel. Di luar Google, peninjau keamanan dapat menelusuri codebase mereka untuk menemukan penggunaan fungsi ini.
Salah satu fitur token ini yang berguna adalah token tersebut dapat diteruskan. Misalnya, 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 mengharuskan
objek secretKeyAccess
tidak null dan memiliki token SecretKeyAccess
yang sebenarnya disimpan. Untuk kunci yang tidak memiliki materi rahasia,
secretKeyAccess
akan diabaikan.
Dengan fungsi tersebut, Anda dapat menulis fungsi yang melakukan serialisasi seluruh kumpulan kunci:
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
Kunci Tink tidak hanya berisi materi kunci mentah, tetapi juga metadata yang menentukan cara kunci harus digunakan (dan, pada gilirannya, kunci tidak boleh digunakan dengan cara lain). Misalnya, kunci RSA SSA PSS di Tink menentukan bahwa kunci RSA ini hanya dapat digunakan dengan algoritma tanda tangan PSS menggunakan fungsi hash yang ditentukan dan panjang garam yang ditentukan.
Terkadang, Anda perlu mengonversi kunci Tink ke dalam format yang berbeda yang mungkin tidak menentukan semua metadata ini secara eksplisit. Hal ini biasanya berarti bahwa metadata harus disediakan saat kunci digunakan. Dengan kata lain, dengan asumsi bahwa kunci selalu digunakan dengan algoritma yang sama, kunci tersebut secara implisit masih memiliki metadata yang sama, hanya disimpan di tempat yang berbeda.
Saat mengonversi kunci Tink ke format lain, Anda harus memastikan bahwa metadata kunci Tink cocok dengan metadata (yang ditentukan secara implisit) dari format kunci lainnya. Jika tidak cocok, konversi harus gagal.
Karena pemeriksaan ini sering kali tidak ada atau tidak lengkap, Tink membatasi akses ke API yang memberikan akses ke materi kunci yang hanya sebagian, tetapi dapat disalahartikan sebagai kunci penuh. Di Java, Tink menggunakan RestrictedApi untuk hal ini, di C++ dan Golang, Tink menggunakan token yang mirip dengan token akses kunci rahasia.
Pengguna API ini bertanggung jawab untuk mencegah serangan penggunaan ulang kunci dan inkompatibilitas.
Anda biasanya menemukan metode yang membatasi "akses kunci sebagian" dalam konteks mengekspor kunci dari atau mengimpor kunci ke Tink. Praktik terbaik berikut menjelaskan cara beroperasi dengan aman dalam skenario ini.
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 don't have Tink. */ byte[] exportTinkHpkeKey(HpkePublicKey key) { return key.getPublicKeyBytes().toByteArray(); }
Ini bermasalah. Setelah menerima kunci, pihak ketiga yang menggunakannya akan membuat beberapa asumsi pada parameter kunci: misalnya, pihak ketiga akan mengasumsikan bahwa algoritma AEAD HPKE yang digunakan untuk kunci 256-bit ini adalah AES-GCM.
Rekomendasi: Pastikan parameter sesuai dengan yang Anda harapkan pada ekspor kunci.
Cara yang lebih baik untuk mengekspor kunci publik:
/** 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(); }
Praktik Terbaik: Gunakan objek Tink sedini mungkin pada impor kunci
Hal ini meminimalkan risiko serangan kebingungan kunci, karena objek Kunci Tink sepenuhnya menentukan algoritma 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(RegistryConfiguration.get(), PublicKeyVerify.class); publicKeyVerify.verify(signature, message); }
Hal ini rentan terhadap error: di lokasi panggilan, sangat mudah untuk melupakan bahwa Anda tidak boleh
menggunakan ecPoint
yang sama dengan algoritma lain. Misalnya, jika fungsi serupa
yang disebut encryptWithECHybridEncrypt
ada, pemanggil mungkin menggunakan
titik kurva yang sama untuk mengenkripsi pesan, yang dapat dengan mudah menyebabkan kerentanan.
Sebagai gantinya, sebaiknya ubah verifyEcdsaSignature
sehingga argumen
pertama adalah EcdsaPublicKey
. Bahkan, setiap kali kunci dibaca dari disk atau
jaringan, kunci harus segera dikonversi menjadi objek EcdsaPublicKey
. Pada
tahap ini, Anda sudah mengetahui cara penggunaan kunci. Jadi, sebaiknya
lakukan commit terhadap kunci tersebut.
Kode sebelumnya dapat ditingkatkan lebih lanjut. Sebaiknya teruskan KeysetHandle
, bukan
EcdsaPublicKey
. Tindakan ini menyiapkan kode
untuk rotasi kunci tanpa pekerjaan tambahan. Jadi, ini harus lebih diutamakan.
Namun, peningkatan ini belum selesai: sebaiknya teruskan
objek PublicKeyVerify
: ini cukup untuk fungsi ini, sehingga meneruskan
objek PublicKeyVerify
berpotensi meningkatkan tempat fungsi
ini dapat digunakan. Namun, pada tahap ini, fungsi menjadi agak sepele
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 kita sepenuhnya menentukan algoritma yang harus digunakan. Pendekatan ini meminimalkan kemungkinan serangan kebingungan kunci.