Tink 不鼓勵使用鍵相關的不當做法,例如:
- 使用者可存取機密金鑰內容:相反地,請盡可能將機密金鑰儲存在 KMS 中,並使用 Tink 支援的系統中預先定義的其中一種方式。
- 使用者存取部分鍵 – 這類操作經常會導致相容性錯誤。
但在某些情況下,我們可能需要違反這些原則。Tink 提供能夠達到這個目標的機制,詳情請參閱以下各節。
密鑰存取權杖
為了存取密鑰素材資源,使用者必須擁有權杖 (通常是某個類別的物件,沒有任何功能)。權杖通常是由方法提供,例如 InsecureSecretKeyAccess.get()
。在 Google 內部,使用者無法使用 Bazel BUILD 可見性 來使用這個函式。在 Google 之外,安全性審查人員可以搜尋自己的程式碼庫,找出這個函式的用法。
這類權杖的另一項實用功能,就是可以傳遞這類權杖。舉例來說,假設您有一個用於序列化任意 Tink 鍵的函式:
String serializeKey(Key key, @Nullable SecretKeyAccess secretKeyAccess);
對於含有密鑰素材的金鑰,此函式要求 secretKeyAccess
物件不得為空值,且必須儲存實際的 SecretKeyAccess
權杖。如果金鑰沒有任何機密資料,系統會忽略 secretKeyAccess
。
有了這類函式,您就可以編寫可序列化整個鍵組的函式:
String serializeKeyset(KeysetHandle keyset, @Nullable SecretKeyAccess
secretKeyAccess);
這個函式會在內部為鍵組中的每個鍵呼叫 serializeKey
,並將指定的 secretKeyAccess
傳遞至基礎函式。之後,如果使用者不需要序列化密鑰內容,就可以呼叫 serializeKeyset
,並使用 null
做為第二個引數。需要序列化密鑰內容的使用者應使用 InsecureSecretKeyAccess.get()
。
金鑰的部分內容存取權
Tink 金鑰不僅包含原始金鑰內容,還包含指定金鑰使用方式 (而且也不應以其他方式使用) 的中繼資料。舉例來說,Tink 中的 RSA SSA PSS 金鑰會指定此 RSA 金鑰只能搭配使用指定的雜湊函式和 salt 長度,搭配使用 PSS 簽署演算法。
有時候,您必須將 Tink 金鑰轉換成可能不會明確指定所有中繼資料的不同格式。這通常代表必須提供金鑰使用時的中繼資料。換句話說,假設金鑰一律搭配相同的演算法使用,則該金鑰仍會隱含地具有相同的中繼資料,只是儲存在不同位置。
將 Tink 金鑰轉換為其他格式時,請務必確認 Tink 金鑰的中繼資料與其他金鑰格式的中繼資料相符 (隱含指定)。如果不相符,轉換就會失敗。
由於這些檢查通常缺少或不完整,Tink 會限制存取 API 的權限,這些 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(); }
這會造成問題。接收金鑰後,使用金鑰的第三方會對金鑰參數做出一些假設:例如,假設用於此 256 位元金鑰的 HPKE AEAD 演算法為 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 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(RegistryConfiguration.get(), PublicKeyVerify.class); publicKeyVerify.verify(signature, message); }
這很容易出錯:在呼叫網站上,很容易忘記,切勿將相同的 ecPoint
與其他演算法一起使用。舉例來說,如果存在名為 encryptWithECHybridEncrypt
的類似函式,呼叫端可能會使用相同的曲線點來加密訊息,這很容易導致安全漏洞。
建議您改為變更 verifyEcdsaSignature
,讓第一個引數為 EcdsaPublicKey
。事實上,每當您從磁碟或網路讀取金鑰時,應立即將其轉換為 EcdsaPublicKey
物件:此時您已經知道金鑰的使用方式,因此建議您修訂金鑰。
上述程式碼還有進步空間。請改為傳入 KeysetHandle
,而非 EcdsaPublicKey
。它可針對金鑰輪替預做準備程式碼,無需進行任何其他工作。因此應建議使用。
然而,這些改善的效果並不會提升,最好是傳入 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 物件,並完全指定應使用的演算法。這種做法可盡量降低發生鍵盤混淆攻擊的可能性。