Exportar o material da chave de forma programática

O Tink desencoraja práticas inadequadas relacionadas a chaves, como:

  • Acesso do usuário ao material da chave secreta: as chaves secretas precisam ser armazenadas em um KMS sempre que possível usando uma das maneiras predefinidas em que o Tink oferece suporte a esses sistemas.
  • Acesso do usuário a partes das chaves: isso geralmente resulta em bugs de compatibilidade.

Na realidade, há casos em que é necessário violar esses princípios. O Tink oferece mecanismos para fazer isso com segurança, conforme descrito nas próximas seções.

Tokens de acesso a chaves secretas

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

Um recurso ú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 com 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 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 de chaves internamente e transmite o secretKeyAccess fornecido para a função subjacente. 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

As chaves Tink não contêm apenas o material bruto, mas também metadados que especificam como a chave deve ser usada (e, por sua vez, que não deve ser usada de nenhuma outra forma). Por exemplo, uma chave RSA SSA PSS no Tink especifica que essa chave RSA só pode ser usada com o algoritmo de assinatura PSS usando a função de hash especificada e o comprimento de sal especificado.

Às vezes, é necessário converter uma chave do Tink em formatos diferentes que podem não especificar explicitamente todos esses metadados. Isso geralmente significa que os metadados precisam ser fornecidos quando a chave é usada. Em outras palavras, supondo que a chave seja sempre usada com o mesmo algoritmo, essa chave ainda tem implicitamente os mesmos metadados, apenas armazenados em um lugar diferente.

Ao converter uma chave do Tink em um formato diferente, é necessário garantir que os metadados da chave do Tink correspondam aos metadados (especificados implicitamente) do outro formato de chave. Se não corresponder, a conversão vai falhar.

Como essas verificações geralmente estão ausentes ou incompletas, o Tink restringe o acesso a APIs que dão acesso ao material-chave, que é apenas parcial, mas pode ser confundido com uma chave completa. No Java, o Tink usa a RestrictedApi para isso. Em C++ e Golang, são usados tokens semelhantes aos tokens de acesso de chave secreta.

Os usuários dessas APIs são responsáveis por evitar incompatibilidades e ataques de reutilização de chaves.

É mais comum encontrar métodos que restringem o "acesso parcial à chave" no contexto de exportação ou importação de chaves para o Tink. As práticas recomendadas a seguir explicam como operar com segurança nesses cenários.

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:

Maneira incorreta de exportar uma chave pública:

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

Isso é problemático. Depois de receber a chave, a parte externa que a usa faz algumas suposições sobre os parâmetros da chave: por exemplo, ela supõe que o algoritmo AEAD HPKE usado para essa chave de 256 bits era AES-GCM.

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

Melhor maneira de exportar uma chave pública:

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

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

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.

Veja o exemplo a seguir:

Uso não tipado:

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

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

Em vez disso, é melhor mudar verifyEcdsaSignature para que o primeiro argumento seja EcdsaPublicKey. Na verdade, sempre que a chave for lida no disco ou na rede, ela precisará ser imediatamente convertida em um objeto EcdsaPublicKey. Neste momento, você já sabe de que maneira 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. Ele prepara o código para a rotação de chaves sem nenhum trabalho extra. Essa é a opção preferencial.

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

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 digitado:

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 ela é lida e especificamos completamente qual algoritmo deve ser usado. Essa abordagem minimiza a probabilidade de ataques de confusão de chaves.