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

Одна из целей 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() .

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

Относительно распространенной ошибкой безопасности является «атака повторного использования ключа». Это может произойти, когда пользователи повторно используют, например, модуль 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();
}