Контроль доступа

Одна из целей Tink — препятствовать плохой практике. Особый интерес в этом разделе представляют два момента:

  1. Tink поощряет использование таким образом, чтобы пользователи не могли получить доступ к материалам секретного ключа. Вместо этого секретные ключи следует по возможности хранить в KMS, используя один из предопределенных способов, которыми Tink поддерживает такие системы.
  2. Tink не рекомендует пользователям получать доступ к частям ключей, поскольку это часто приводит к ошибкам совместимости.

На практике, конечно, оба этих принципа иногда приходится нарушать. Для этого Tink предоставляет разные механизмы.

Токены доступа к секретному ключу

Чтобы получить доступ к материалам секретного ключа, пользователям необходим токен (который обычно представляет собой просто объект какого-либо класса без какой-либо функциональности). Токен обычно предоставляется таким методом, как InsecureSecretKeyAccess.get() . В Google пользователи не могут использовать эту функцию с помощью видимости Bazel BUILD . За пределами Google эксперты по безопасности могут искать в своей кодовой базе использование этой функции.

Одной из полезных особенностей этих токенов является то, что их можно передавать другим людям. Например, предположим, что у вас есть функция, которая сериализует произвольный ключ Tink:

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

Для ключей, которые имеют секретный ключевой материал, эта функция требует, чтобы объект secretKeyAccess был ненулевым и содержал действительный токен SecretKeyAccess . Для ключей, не имеющих секретного материала, secretKeyAccess игнорируется.

Имея такую ​​функцию, можно написать функцию, которая сериализует весь набор ключей: StringserializeKeyset(KeysetHandle keyset, @Nullable SecretKeyAccess secretKeyAccess);

Эта функция внутренне вызывает serializeKey для каждого ключа в наборе ключей и передает заданный secretKeyAccess базовой функции. Пользователи, которые затем вызывают serializeKeyset без необходимости сериализации материала секретного ключа, могут использовать null в качестве второго аргумента. Пользователи, которым необходимо сериализовать материал секретного ключа, должны использовать InsecureSecretKeyAccess.get() .

Доступ к частям ключа

Ключи Tink содержат не только исходный материал ключа, но и некоторые метаданные, которые точно определяют, как следует использовать ключ. Ключ не должен использоваться каким-либо другим способом. Например, ключ RSA SSA PSS в Tink указывает, что этот ключ RSA можно использовать только с алгоритмом подписи PSS, использующим указанную хеш-функцию и указанную длину соли.

Иногда необходимо преобразовать ключ Tink в другие форматы, которые могут не указывать явно все эти метаданные. Обычно это означает, что метаданные необходимо предоставлять при использовании ключа. Таким образом (при условии, что ключ всегда используется с одним и тем же алгоритмом) такой ключ по-прежнему неявно имеет одни и те же метаданные, просто он хранится в другом месте.

Когда вы конвертируете ключ Tink в другой формат, вам необходимо убедиться, что метаданные ключа Tink соответствуют (возможно, неявно указанным) метаданным другого формата ключа. Если оно не соответствует, преобразование должно завершиться неудачно.

Поскольку эти проверки часто отсутствуют или являются неполными, Tink ограничивает доступ к 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();
}

Это проблематично. После получения ключа третья сторона, использующая его, делает некоторые предположения о параметрах ключа: например, она предполагает, что алгоритм HPKE AEAD, использованный для этого 256-битного ключа, был 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 при его чтении и полностью указываем, какой алгоритм следует использовать. Такой подход сводит к минимуму вероятность атак с путаницей ключей.