存取權控管

Tink 的其中一個目標就是勸阻不當行為。本節特別值得注意的是兩點:

  1. Tink 會鼓勵使用者以無法存取密鑰內容的方式使用這些資料。而應盡可能使用其中一種支援這類系統的預先定義方法,將密鑰儲存在 KMS 中。
  2. Tink 建議使用者存取金鑰的某些部分,因為這樣通常會發生相容性錯誤。

實務上,有時必須違反這兩項原則。為此,Tink 提供不同的機制

密鑰存取權杖

如要存取密鑰內容,使用者必須取得權杖 (通常只是某個類別的物件,不含任何功能)。權杖通常是由 InsecureSecretKeyAccess.get() 等方法提供。在 Google 中,使用者無法透過 Bazel BUILD 瀏覽權限使用這項功能。除了 Google 以外,安全性審查人員也可以自行搜尋程式碼集,瞭解這項功能的使用情形。

權杖的優點是可以傳遞。例如,假設您有可將任意 Tink 金鑰序列化的函式:

String serializeKey(Key key, @Nullable SecretKeyAccess secretKeyAccess);

對於含有密鑰內容的金鑰,這個函式會要求 secretKeyAccess 物件不是空值,並實際儲存 SecretKeyAccess 權杖。如果金鑰沒有任何密鑰內容,系統會忽略 secretKeyAccess

以這個函式來說,您可以編寫將整個金鑰組序列化的函式:String serializeKeyset(KeysetHandle keyset, @Nullable SecretKeyAccesssecretKeyAccess);

這個函式會在內部為金鑰組中的每個鍵呼叫 serializeKey,並將指定的 secretKeyAccess 傳遞至基礎函式。接著,無需將密鑰內容序列化,即可呼叫 serializeKeyset 的使用者,可以使用 null 做為第二個引數。如果使用者必須將密鑰內容序列化,就必須使用 InsecureSecretKeyAccess.get()

存取金鑰部分

另一個常見的安全性錯誤是「金鑰重複使用攻擊」。如果在兩個不同設定中 (例如運算簽名和加密) 重複使用模數 n 和 RSA 金鑰的模數 de 等指數,就可能會發生這種情況1

處理加密編譯金鑰時,另一個相對常見的錯誤是指定金鑰的一部分,然後「假設」中繼資料。舉例來說,假設使用者想從 Tink 匯出 RSASSA-PSS 公開金鑰,以便與其他程式庫搭配使用。在 Tink 中,這些索引鍵具有下列部分:

  • 模數 n
  • 公開指數 e
  • 內部使用的兩個雜湊函式規格
  • 演算法內部使用的鹽長度。

匯出這類鍵時,您可能會忽略雜湊函式和鹽長度。這通常可以正常運作,因為其他程式庫通常不會要求使用雜湊函式 (例如只是假設使用 SHA256),且 Tink 中使用的雜湊函式幾乎與其他程式庫相同 (或者我們特別選擇了雜湊函式,以便與其他程式庫搭配運作)。

然而,忽略雜湊函式將可能造成昂貴的錯誤。為了看一下這個情況,假設之後又在 Tink 金鑰組中加入具有不同雜湊函式的新金鑰。接著,假設金鑰是以方法匯出,並提供給業務合作夥伴,以便與其他程式庫搭配使用。Tink 現在假設是不同的內部雜湊函式,而且無法驗證簽名。

在這種情況下,如果雜湊函式與其他程式庫的預期不符,匯出金鑰的函式就會失敗:否則,匯出的金鑰會無用,因為會產生不相容的密文或簽名。

為防止發生這類錯誤,Tink 會限制提供僅部分金鑰內容存取權的函式,但該函式可能會誤認為完整金鑰。例如,在 Java Tink 中,使用 RestrictedApi 存在。

當使用者使用這類註解時,必須負責防止金鑰重複使用攻擊和不相容的問題。

最佳做法:在匯入金鑰時盡早使用 Tink 物件

將金鑰從金鑰匯出或匯入 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(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 物件,並完整指定要使用的演算法。這種做法可將金鑰混淆攻擊的可能性降到最低。

最佳做法:驗證金鑰匯出功能中的所有參數

舉例來說,如果您編寫的函式會匯出 HPKE 公開金鑰:

匯出公開金鑰的無效方式:

/** Provide the key to our users which do not have Tink. */
byte[] exportTinkHpkeKey(HpkePublicKey key) {
    return key.getPublicKeyBytes().toByteArray();
}

這會造成問題。收到金鑰後,使用金鑰的第三方會對該金鑰的參數做出假設:例如,假設這個金鑰 256 位元使用的 HPKE AEAD 演算法是 AES-GCM,以此類推。

建議:確認參數符合金鑰匯出工具預期的參數。

更好的匯出公開金鑰:

/** Provide the key to our users which do not 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 do not add a Tink style prefix
    if (!key.getParameters().getVariant().equals(HpkeParameters.Variant.NO_PREFIX)) {
        throw new IllegalArgumentException("Bad parameters");
    }
    return key.getPublicKeyBytes().toByteArray();
}