访问权限控制

Tink 的一个目标是阻止不良行为。尤其适合 这一部分包含两点:

  1. Tink 鼓励用户以无法访问密钥的方式使用密钥 材料。相反,密钥应存储在 KMS(尽可能) 使用 Tink 支持此类系统的某种预定义方式。
  2. Tink 会阻止用户访问密钥的某些部分,因为他们经常这样做 会导致兼容性错误

在实践中,当然有时不得不违反这两个原则。 为此,Tink 提供了不同的机制。

密钥访问令牌

要访问密钥材料,用户必须拥有令牌( 通常只是某个类的对象,没有任何功能)。 令牌通常由如下方法提供: InsecureSecretKeyAccess.get()。介于 用户无法使用 Bazel build 可见性。 除了 Google 之外,安全审核人员还可以在其代码库中搜索 此函数。

这些令牌的一个实用功能是它们可以传递。例如: 假设您有一个对任意 Tink 键进行序列化的函数:

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

对于具有密钥材料的密钥,该函数需要 secretKeyAccess 对象为非 null 且具有实际 SecretKeyAccess 存储令牌。对于不含任何密钥材料的密钥, 系统会忽略 secretKeyAccess

给定这样的函数,可以编写一个将 整个密钥集: StringSerializeKeyset(KeysetHandle keyset, @Nullable SecretKeyAccess secretKeyAccess);

此函数为密钥集中的每个按键调用 serializeKey 并将指定的 secretKeyAccess 传递给底层函数。 随后调用 serializeKeyset 而无需进行序列化的用户 密钥材料可以使用 null 作为第二个参数。用户 需要序列化密钥材料 InsecureSecretKeyAccess.get()

访问密钥的各个部分

Tink 密钥不仅包含原始密钥材料,也包含一些 确切指定了应如何使用相应密钥。不应使用该密钥 以任何其他方式存储数据例如,Tink 中的 RSA SSA PSS 密钥指定了 此 RSA 密钥只能与使用 指定的哈希函数和指定的盐长度。

有时,需要将 Tink 密钥转换成其他格式, 不能明确指定所有这些元数据。这通常意味着 元数据。因此(假设键为 始终与同一算法一起使用) 但它只是存储在其他位置

在将 Tink 密钥转换为其他格式时 确保 Tink 键的元数据与(可能隐式指定)的 另一种密钥格式的元数据。如果不匹配,则转换必须失败。

由于这些检查经常缺失或不完整,Tink 限制了 提供对部分密钥材料的访问权限的 API, 会误认为是完整的键。在 Java 中,Tink 使用 RestrictedApi,在 C++ 和 Golang 中,它使用令牌 类似于密钥访问令牌

这些 API 的用户需负责阻止 密钥重用攻击和不兼容性问题

您最常遇到的是受“部分键”限制的方法 访问权限”从 Tink 导出密钥或将密钥导入到 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(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 对象 并且我们会完整指定应使用的算法。 这种方法可最大限度地降低密钥混淆攻击的可能性。