Одна из целей Tink — препятствовать плохой практике. Особый интерес в этом разделе представляют два момента:
- Tink поощряет использование таким образом, чтобы пользователи не могли получить доступ к материалам секретного ключа. Вместо этого секретные ключи следует по возможности хранить в KMS , используя один из предопределенных способов, которыми Tink поддерживает такие системы.
- 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()
.
Доступ к частям ключа
Относительно распространенной ошибкой безопасности является «атака повторного использования ключа». Это может произойти, когда пользователи повторно используют, например, модуль n
и показатели степени d
и e
ключа RSA в двух разных настройках (например, для вычисления подписей и шифрования) 1 .
Другая относительно распространенная ошибка при работе с криптографическими ключами — указать часть ключа, а затем «предполагать» метаданные. Например, предположим, что пользователь хочет экспортировать открытый ключ RSASSA-PSS из Tink для использования с другой библиотекой. В 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(); }
Это проблематично. После получения ключа третья сторона, использующая его, делает некоторые предположения о параметрах ключа: например, она предполагает, что алгоритм HPKE AEAD, использованный для этого 256-битного ключа, был 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(); }