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

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