Tink desaconseja las prácticas recomendadas relacionadas con las claves, como las que se mencionan a continuación:
- Acceso de los usuarios al material de la clave secreta: En cambio, las claves secretas deben almacenarse en un KMS siempre que sea posible con una de las formas predefinidas en las que Tink admite esos sistemas.
- Acceso de los usuarios a partes de las claves: Hacerlo suele generar errores de compatibilidad.
En realidad, hay casos que requieren incumplir estos principios. Tink proporciona mecanismos para hacerlo de forma segura, que se describen en las siguientes secciones.
Tokens de acceso de clave secreta
Para acceder al material de la clave secreta, los usuarios deben tener un token (que suele ser un objeto de alguna clase, sin ninguna funcionalidad). Por lo general, el token se proporciona a través de un método como InsecureSecretKeyAccess.get()
. En Google, se impide que los usuarios usen esta función con la visibilidad de compilación de Bazel. Fuera de Google, los revisores de seguridad pueden buscar en su base de código el uso 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 de Tink arbitraria:
String serializeKey(Key key, @Nullable SecretKeyAccess secretKeyAccess);
Para 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. Para las claves que no tienen ningún material secreto, se ignora el secretKeyAccess
.
Con esta función, es posible escribir una función que serializa un conjunto de claves completo:
String serializeKeyset(KeysetHandle keyset, @Nullable SecretKeyAccess
secretKeyAccess);
Esta función llama a serializeKey
para cada clave del conjunto de claves de forma interna y pasa el secretKeyAccess
determinado a la función subyacente. Los usuarios que luego llamen a serializeKeyset
sin necesidad de serializar el material de la clave secreta pueden usar null
como segundo argumento. Los usuarios que necesiten serializar material de claves secretas deben usar InsecureSecretKeyAccess.get()
.
Acceso a partes de una clave
Las claves de Tink no solo contienen material de clave sin procesar, sino también metadatos que especifican cómo se debe usar la clave (y, a su vez, que no se debe usar de ninguna otra manera). Por ejemplo, una clave PSS de SSA RSA en Tink especifica que esta clave RSA solo se puede usar con el algoritmo de firma PSS con la función de hash y la longitud de sal especificadas.
A veces, es necesario convertir una clave Tink a diferentes formatos que pueden no especificar explícitamente todos estos metadatos. Por lo general, esto significa que se deben proporcionar los metadatos cuando se usa la clave. En otras palabras, si suponemos que la clave siempre se usa con el mismo algoritmo, esta clave aún tiene implícitamente los mismos metadatos, solo se almacena en un lugar diferente.
Cuando conviertes una clave de Tink a un formato diferente, debes asegurarte de que los metadatos de la clave de Tink coincidan con los metadatos (especificados de forma implícita) del otro formato de clave. Si no coinciden, la conversión debe fallar.
Debido a que estas verificaciones suelen faltar o estar incompletas, Tink restringe el acceso a las APIs que proporcionan acceso al material de clave, que es solo parcial, pero que podría confundirse con una clave completa. En Java, Tink usa RestrictedApi para esto. En C++ y Golang, usa tokens similares a los tokens de acceso de clave secreta.
Los usuarios de estas APIs son responsables de evitar los ataques de reutilización de claves y las incompatibilidades.
Por lo general, te encuentras con métodos que restringen el "acceso parcial a la clave" en el contexto de la exportación o importación de claves a Tink. En las siguientes prácticas recomendadas, se explica cómo operar de forma segura en estas situaciones.
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 de HPKE, sucede lo siguiente:
Mala forma de exportar una clave pública:
/** Provide the key to our users which don't have Tink. */ byte[] exportTinkHpkeKey(HpkePublicKey key) { return key.getPublicKeyBytes().toByteArray(); }
Esto es un problema. Después de recibir la clave, el tercero que la usa hace algunas suposiciones sobre los parámetros de la clave: por ejemplo, supondrá que el algoritmo de AEAD de HPKE que se usó para esta clave de 256 bits fue AES-GCM.
Recomendación: Verifica que los parámetros sean los que esperas en la exportación de claves.
Mejor forma de exportar una clave 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áctica recomendada: Usa los objetos Tink lo antes posible en la importación de claves.
Esto minimiza el riesgo de ataques de confusión de claves, ya que el objeto de clave de Tink especifica por completo el algoritmo correcto y almacena todos los metadatos junto con el material de la clave.
Consulta el siguiente ejemplo:
Uso no 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); }
Esto es propenso a errores: en el sitio de 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 fácilmente.
En su lugar, es mejor cambiar verifyEcdsaSignature
para que el primer argumento sea EcdsaPublicKey
. De hecho, cada vez que se lee la clave 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 que
te comprometas con ella.
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. Por lo tanto, esta es la opción preferida.
Sin embargo, las mejoras no terminan ahí: es aún mejor pasar el objeto PublicKeyVerify
, ya que es suficiente para esta función, por lo que pasar el objeto PublicKeyVerify
podría aumentar los lugares donde 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 se lea el material de clave 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 de claves.