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 金鑰,只能與使用指定雜湊函式和指定鹽長度的 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
物件:此時您已經知道金鑰的使用方式,因此建議您修訂金鑰。
上述程式碼還可以進一步改善。比起傳入 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 物件,並且完整指定應使用的演算法。這種做法可盡量降低發生鍵盤混淆攻擊的可能性。