Control de acceso

Uno de los objetivos de Tink es desalentar las prácticas no recomendadas. En esta sección, encontrarás dos puntos de particular interés:

  1. Tink incentiva el uso para que los usuarios no puedan acceder al material de la clave secreta. En su lugar, las claves secretas deben almacenarse en un KMS siempre que sea posible mediante una de las formas predefinidas en las que Tink admite esos sistemas.
  2. Tink disuade a los usuarios de acceder a partes de claves, ya que esto suele generar errores de compatibilidad.

Por supuesto, en la práctica, a veces se deben infringir ambos principios. Para ello, Tink proporciona diferentes mecanismos.

Tokens de acceso a claves secretas

Para acceder al material de las claves secretas, los usuarios deben tener un token (que, por lo general, solo es un objeto de alguna clase, sin ninguna funcionalidad). Por lo general, un método como InsecureSecretKeyAccess.get() proporciona el token. En Google, los usuarios no pueden usar esta función mediante la visibilidad de COMPILACIÓN de Bazel. Fuera de Google, los revisores de seguridad pueden buscar en su base de código los usos de esta función.

Una función útil de estos tokens es que se pueden pasar. Por ejemplo, supongamos que tienes una función que serializa una clave Tink arbitraria:

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

En el caso de las claves que tienen material de clave secreta, esta función requiere que el objeto secretKeyAccess no sea nulo y que tenga almacenado un token SecretKeyAccess real. En el caso de las claves que no tienen material secreto, se ignora secretKeyAccess.

Con esa función, es posible escribir una función que serialice un conjunto de claves completo: String serializeKeyset(KeysetHandle keyset, @Nullable SecretKeyAccess secretKeyAccess);

Esta función llama internamente a serializeKey para cada clave del conjunto de claves y pasa el secretKeyAccess determinado a la función subyacente. Los usuarios que luego llamen a serializeKeyset sin la necesidad de serializar el material de la clave secreta pueden usar null como segundo argumento. Los usuarios que tienen la necesidad de serializar el material de la clave secreta deben usar InsecureSecretKeyAccess.get().

Acceso de partes de una clave

Un error de seguridad relativamente común es un “ataque de reutilización de claves”. Esto puede ocurrir cuando los usuarios vuelven a usar, por ejemplo, el módulo n y los exponentes d y e de una clave RSA en dos configuraciones diferentes (p.ej., para calcular firmas y encriptaciones)1.

Otro error relativamente común cuando se trata de claves criptográficas es especificar parte de la clave y, luego, “suponer” los metadatos. Por ejemplo, supongamos que un usuario desea exportar una clave pública RSASSA-PSS desde Tink para usarla con otra biblioteca. En Tink, estas claves tienen las siguientes partes:

  • Módulo n
  • El exponente público e
  • La especificación de las dos funciones hash usadas internamente
  • La longitud de la sal usada internamente en el algoritmo.

Cuando exportes una clave de este tipo, puedes ignorar las funciones hash y la longitud de la sal. Esto a menudo puede funcionar bien, ya que otras bibliotecas no suelen solicitar las funciones hash (y, por ejemplo, solo suponen que se usa SHA256), y la función hash utilizada en Tink es casualmente la misma que en la otra biblioteca (o quizás se eligieron específicamente las funciones hash para que funcionen en conjunto con la otra biblioteca).

Sin embargo, ignorar las funciones hash sería un error potencialmente costoso. Para ver esto, supongamos que más adelante se agrega una clave nueva con una función hash diferente al conjunto de claves Tink. Supongamos que, luego, la clave se exporta con el método y se proporciona a un socio comercial, que la usa con la otra biblioteca. Tink ahora asume una función hash interna diferente y no puede verificar la firma.

En este caso, la función que exporta la clave debería fallar si la función hash no coincide con lo que espera la otra biblioteca. De lo contrario, la clave exportada no tiene utilidad, ya que crea firmas o textos cifrados incompatibles.

Para evitar estos errores, Tink restringe las funciones que otorgan acceso al material de la clave, que es solo parcial, pero podría confundirse como una clave completa. Por ejemplo, Tink usa RestrictedApi en Java para esto.

Cuando un usuario usa una anotación de este tipo, es responsable de evitar los ataques de reutilización de claves y las incompatibilidades.

Práctica recomendada: Usa objetos Tink lo antes posible en la importación de claves

Por lo general, encontrarás métodos que están restringidos con el “acceso parcial a la clave” cuando se exportan claves o se importan a Tink.

Esto minimiza el riesgo de ataques de confusión de claves, ya que el objeto Tink Key especifica por completo el algoritmo correcto y almacena todos los metadatos junto con el material de la clave.

Consulta el siguiente ejemplo:

Uso sin escritura:

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

Esto es propenso a errores: en el sitio de la llamada, es muy fácil olvidar que nunca debes usar el mismo ecPoint con otro algoritmo. Por ejemplo, si existe una función similar llamada encryptWithECHybridEncrypt, el llamador podría usar el mismo punto de curva para encriptar un mensaje, lo que puede generar vulnerabilidades con facilidad.

En cambio, es mejor cambiar verifyEcdsaSignature para que el primer argumento sea EcdsaPublicKey. De hecho, cada vez que la clave se lee desde el disco o la red, se debe convertir de inmediato en un objeto EcdsaPublicKey: en este punto, ya sabes de qué manera se usa la clave, por lo que es mejor confirmarla.

El código anterior se puede mejorar aún más. En lugar de pasar un EcdsaPublicKey, es mejor pasar un KeysetHandle. Prepara el código para la rotación de claves sin ningún trabajo adicional. Es lo que debería ser la opción preferida.

Sin embargo, las mejoras no se realizaron. Es aún mejor pasar el objeto PublicKeyVerify. Esto es suficiente para esta función, por lo que pasar el objeto PublicKeyVerify podría aumentar los lugares en los que se puede usar esta función. Sin embargo, en este punto, la función se vuelve bastante trivial y se puede intercalar.

Recomendación: Cuando el material de la clave se lea desde el disco o la red por primera vez, crea los objetos Tink correspondientes lo antes posible.

Uso escrito:

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

Con ese código, convertimos inmediatamente el array de bytes en un objeto Tink cuando se lee y especificamos por completo qué algoritmo se debe usar. Este enfoque minimiza la probabilidad de ataques de confusión clave.

Práctica recomendada: Verifica todos los parámetros en la exportación de claves

Por ejemplo, si escribes una función que exporta una clave pública HPKE, ocurrirá lo siguiente:

No es una buena forma de exportar una clave pública:

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

Esto es problemático. Después de recibir la clave, el tercero que la usa hace suposiciones sobre los parámetros de la clave: por ejemplo, supondrá que el algoritmo HPKE AEAD que se usó para esta clave de 256 bits era AES-GCM, y así sucesivamente.

Recomendación: Verifica que los parámetros sean los que esperas para la exportación de claves.

Mejor forma de exportar una clave 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();
}