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

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