以程式輔助方式匯出金鑰內容

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 物件,並且完整指定應使用的演算法。這種做法可盡量降低發生鍵盤混淆攻擊的可能性。