Программный экспорт ключевого материала

Тинк не поощряет неправильные действия, связанные с ключами, такие как:

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

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

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

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

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

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

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

Имея такую ​​функцию, можно написать функцию, которая сериализует весь набор ключей:

String serializeKeyset(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(RegistryConfiguration.get(), 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 при его чтении и полностью указываем, какой алгоритм следует использовать. Такой подход сводит к минимуму вероятность атак с путаницей ключей.