Controle de acesso

Um dos objetivos da Tink é desencorajar práticas ruins. Há dois pontos importantes nesta seção:

  1. A Tink incentiva o uso de modo que os usuários não possam acessar o material da chave secreta. Em vez disso, as chaves secretas precisam ser armazenadas em um KMS sempre que possível usando uma das maneiras predefinidas com que o Tink oferece suporte a esses sistemas.
  2. A Tink desencoraja os usuários de acessar partes das chaves, porque isso geralmente resulta em bugs de compatibilidade.

Na prática, é claro que ambos os princípios precisam ser violados algumas vezes. Para isso, a Tink oferece diferentes mecanismos.

Tokens de acesso de chave secreta

Para acessar o material da chave secreta, os usuários precisam ter um token, que normalmente é apenas um objeto de alguma classe, sem qualquer funcionalidade. O token geralmente é fornecido por um método como InsecureSecretKeyAccess.get(). No Google, os usuários não podem usar essa função por meio da visibilidade do Bazel BUILD. Fora do Google, os revisores de segurança podem pesquisar a base de código para conferir os usos dessa função.

Uma característica útil desses tokens é que eles podem ser transmitidos. Por exemplo, suponha que você tenha uma função que serializa uma chave Tink arbitrária:

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

Para chaves que têm material de chave secreta, essa função exige que o objeto secretKeyAccess não seja nulo e tenha um token SecretKeyAccess real armazenado. Para chaves que não têm nenhum material secreto, o secretKeyAccess é ignorado.

Com essa função, é possível escrever uma função que serializa um conjunto de chaves inteiro: String serializeKeyset(KeysetHandle keyset, @Nullable SecretKeyAccess secretKeyAccess);

Essa função chama serializeKey para cada chave no conjunto internamente e transmite o secretKeyAccess especificado para a função. Os usuários que chamam serializeKeyset sem a necessidade de serializar o material da chave secreta podem usar null como o segundo argumento. Os usuários que precisam serializar o material da chave secreta precisam usar InsecureSecretKeyAccess.get().

Acesso a partes de uma chave

Um bug de segurança relativamente comum é um "ataque de reutilização de chaves". Isso pode ocorrer quando os usuários reutilizam, por exemplo, o módulo n e os expoentes d e e de uma Chave RSA em duas configurações diferentes (por exemplo, para calcular assinaturas e criptografias)1.

Outro erro relativamente comum ao lidar com chaves criptográficas é especificar parte da chave e, em seguida, "presumir" os metadados. Por exemplo, suponha que um usuário queira exportar uma chave pública RSASSA-PSS do Tink para usar com uma biblioteca diferente. No Tink, essas chaves têm as seguintes partes:

  • O módulo n
  • O expoente público e
  • A especificação das duas funções hash usadas internamente
  • O comprimento do sal usado internamente no algoritmo.

Ao exportar essa chave, ignore as funções hash e o comprimento do sal. Isso pode funcionar bem, já que outras bibliotecas não pedem as funções hash, por exemplo, apenas presumindo que o SHA256 é usado. Além disso, a função hash usada no Tink é coincidentemente igual à da outra biblioteca (ou talvez as funções hash tenham sido escolhidas especificamente para funcionar em conjunto com a outra biblioteca).

No entanto, ignorar as funções hash seria um erro potencialmente caro. Para verificar isso, suponha que mais tarde uma nova chave com uma função hash diferente seja adicionada ao conjunto de chaves do Tink. Suponha que, então, a chave seja exportada com o método e fornecida a um parceiro de negócios, que a usa com a outra biblioteca. O Tink agora assume uma função hash interna diferente e não pode verificar a assinatura.

Nesse caso, a função que exporta a chave falhará se a função de hash não corresponder ao que a outra biblioteca espera. Caso contrário, a chave exportada será inútil porque cria textos ou assinaturas criptografados incompatíveis.

Para evitar esses erros, a Tink restringe as funções que dão acesso ao material da chave, que é apenas parcial, mas pode ser confundido com uma chave completa. Por exemplo, no Java, o Tink usa RestrictedApi para isso.

Quando um usuário usa essa anotação, ele é responsável por impedir incompatibilidades e ataques de reutilização de chaves.

Prática recomendada: usar objetos Tink o quanto antes na importação de chaves

Geralmente, você encontra métodos restritos com "acesso a chave parcial" ao exportar ou importar chaves para o Tink.

Isso minimiza o risco de ataques de confusão de chaves, porque o objeto Tink Key especifica totalmente o algoritmo correto e armazena todos os metadados com o material da chave.

Confira o exemplo a seguir:

Uso sem tipo:

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);
}

Isso é propenso a erros: no local da chamada, é muito fácil esquecer que você nunca deve usar o mesmo ecPoint com outro algoritmo. Por exemplo, se existir uma função semelhante chamada encryptWithECHybridEncrypt, o autor da chamada poderá usar o mesmo ponto de curva para criptografar uma mensagem, o que pode facilmente levar a vulnerabilidades.

Em vez disso, é melhor mudar verifyEcdsaSignature para que o primeiro argumento seja EcdsaPublicKey. Na verdade, sempre que a chave é lida no disco ou na rede, ela precisa ser convertida imediatamente em um objeto EcdsaPublicKey. Nesse momento, você já sabe como a chave é usada, então é melhor se comprometer com ela.

O código anterior pode ser melhorado ainda mais. Em vez de transmitir um EcdsaPublicKey, é melhor transmitir um KeysetHandle. Ela prepara o código para a rotação de chaves sem nenhum trabalho extra. Portanto, essa é a opção preferencial.

No entanto, as melhorias não foram feitas. É ainda melhor transmitir o objeto PublicKeyVerify: isso é suficiente para essa função, então transmitir o objeto PublicKeyVerify pode aumentar os lugares em que essa função pode ser usada. Neste ponto, no entanto, a função se torna bastante trivial e pode ser in-line.

Recomendação:quando o material da chave for lido do disco ou da rede pela primeira vez, crie os objetos Tink correspondentes o mais rápido possível.

Uso tipado:

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();
}

Usando esse código, convertemos imediatamente a matriz de bytes em um objeto Tink quando ele é lida, e especificamos totalmente qual algoritmo será usado. Essa abordagem minimiza a probabilidade de ataques de confusão importante.

Prática recomendada: verificar todos os parâmetros na exportação de chaves

Por exemplo, se você escrever uma função que exporta uma chave pública HPKE:

Não é possível exportar uma chave pública:

/** Provide the key to our users which do not have Tink. */
byte[] exportTinkHpkeKey(HpkePublicKey key) {
    return key.getPublicKeyBytes().toByteArray();
}

Isso é um problema. Depois de receber a chave, o terceiro que a utiliza faz algumas suposições sobre os parâmetros da chave. Por exemplo, presume que o algoritmo HPKE AEAD usado para essa chave de 256 bits era AES-GCM e assim por diante.

Recomendação:verifique se os parâmetros são os esperados na exportação da chave.

Melhor maneira de exportar uma chave pública:

/** 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();
}