Interfaces y primitivas

A continuación, definiremos (de manera informal, pero más formalmente) dos partes importantes del lenguaje que se usa en Tink: el Primitive y la Interface.

Básico

Un primitivo es un objeto matemático que corresponde a todos los algoritmos que realizan una tarea de forma segura. Por ejemplo, la primitiva de AEAD consta de todos los algoritmos de encriptación que satisfacen las propiedades de seguridad que Tink requiere de un Aead.

Hacemos hincapié en que las primitivas no están vinculadas a un lenguaje de programación ni a una forma específica de acceder a ellos. En cambio, se debe pensar en lo primitivo como un objeto puramente matemático. Por ejemplo, si consideramos el AEAD, en esencia, constará de pares de funciones: una que realiza la encriptación y otra que realiza la desencriptación.

Interfaces

Una interfaz es una manera en la que proporcionamos a los usuarios acceso a una primitiva. Por ejemplo, se espera que, en el futuro, Tink proporcione una interfaz Mac, pero también una interfaz StreamingMac, que permitirá procesar el Mac de datos que no se carga directamente en la memoria.

Ten en cuenta que aquí distinguimos explícitamente interfaces y primitivas. Esto debería dejar en claro que el objeto matemático al que estas dos interfaces otorgan acceso es el mismo.

Definiciones formales

Para la mayoría de los lectores, las explicaciones intuitivas anteriores son probablemente suficientes. Sin embargo, creemos que, a veces, puede ser importante proporcionar definiciones formales de estos conceptos.

Funciones criptográficas

El concepto de función criptográfica no es tan importante como el concepto de primitiva, pero debemos ingresarlo para definir formalmente la primitiva.

Función criptográfica

Una función criptográfica es un mapa

\[ f: {\bf K} \times {\bf R} \times {\bf I} \to {\bf O}\]

de un conjunto \({\bf K}\) (el espacio de claves), un conjunto \({\bf R} = \{0,1\}^{\infty}\)(aleatorización, que suponemos que es el conjunto de strings de bits infinitas), y un conjunto \({\bf I}\) (el espacio de entrada), a un conjunto \({\bf O}\) (el espacio de salida).

Más adelante, quedará claro por qué agregamos un parámetro de aleatoriedad específico.

Como ejemplo, mostramos una posibilidad de cómo estos conceptos pueden asignarse a AES-GCM. Para cada tamaño de clave válido \(s_k\), tamaño del nonce \(s_n\)y tamaño de etiqueta\(s_t\), AES-GCM consta de dos funciones criptográficas, una para la encriptación y otra para la desencriptación. Ambos tendrán el mismo espacio de claves \({\bf K} = \{0,1\}^{s_k}\).

Para la función de encriptación \(\mathrm{Enc}\), se usarán los primeros \(s_n\) bits de aleatoriedad a fin de seleccionar el nonce.

Deja que \({\bf B} = \{0,1\}^8\) denota un byte. El espacio de entrada de la función de encriptación son los pares \({\bf I} = {\bf B}^{*} \times {\bf B}^{*}\) de pares de strings de bytes de longitud arbitraria. El primer elemento del par está destinado a ser el mensaje, el segundo elemento son los datos asociados. El estándar AES-GCM tiene un límite superior para la longitud de las entradas, pero preferimos permitir longitudes arbitrarias y, en su lugar, agregar un símbolo de error especial \(\bot\) al espacio de salida. El espacio de salida se convierte en \({\bf O} = {\bf B}^* \cup \{\bot\}\), en el que definimos de forma arbitraria el resultado de los cálculos exitosos como la concatenación \((\mathrm{IV} \| \mathrm{ciphertext} \| \mathrm{tag})\) como se indica en el estándar, y el\(\bot\)de salida, en caso de que alguna entrada sea demasiado larga. Por lo tanto, para una clave fija, la función de encriptación se convierte en de tipo \(\mathrm{Enc}_k : {\bf R} \times {\bf B}^* \times {\bf B}^* \rightarrow {\bf B}^* \cup \{\bot\}\).

En el caso de la función de desencriptación \(\mathrm{Dec}\) el espacio de claves es el mismo. El espacio de entrada, casualmente, es el mismo: \({\bf I} ={\bf B}^* \times {\bf B}^*\), pero ahora el primer elemento debe ser el resultado de la función de encriptación, mientras que el segundo sigue siendo los datos asociados.

El espacio de salida también es el mismo \({\bf O} = {\bf B}^* \cup \{\bot\}\) (de nuevo, una coincidencia). La interpretación es un poco diferente, ya que \(\bot\) , por lo general, denota un error de autenticación (aunque también será el resultado en caso de que alguna entrada sea demasiado larga).

Destacamos que la formalización anterior no es la única opción para formalizar el estándar. Por ejemplo, uno podría considerar el nonce como parte de la entrada, en lugar de leerlo desde la aleatoriedad (lo que da como resultado una primitiva muy diferente). Como alternativa, se podría definir el resultado como un triple que contenga el nonce, el texto cifrado y la etiqueta (en lugar de la concatenación). También podrías restringir el espacio de claves (de manera arbitraria) a\({\bf K} = \{0,1\}^{128} \cup \{0,1\}^{256}\).

Algoritmo criptográfico:

Un algoritmo criptográfico (simétrico) es una tupla

\[(f_1, ... f_k)\]

de funciones criptográficas, en las que todas las funciones tienen el mismo espacio de claves. El tipo de algoritmo criptográfico es la tupla \((({\bf I}_1, {\bf O}_1), \ldots, ({\bf I}_k, {\bf O}_k))\).

Por ejemplo, para cada triple \((s_k, s_n, s_t)\) válido de clave, nonce y tamaño de etiqueta, AES-GCM\({}_{s_k, s_n, s_t}\) es un algoritmo criptográfico con las dos funciones \(\mathrm{Enc}\) y \(\mathrm{Dec}\) que se describieron antes.

Interfaces y datos primitivos

A continuación, definiremos una primitiva criptográfica.

Básico
Una primitiva es un conjunto de algoritmos criptográficos en el que todos los algoritmos tienen el mismo tipo \((({\bf I}_1, {\bf O}_1), \ldots, ({\bf I}_k, {\bf O}_k))\)y los espacios clave de los algoritmos son inconexos en pares.

Como ejemplo, considera las \(\mathrm{AEAD}\) primitivas de Tink. Tiene varios algoritmos, entre ellos, AES-GCM para tamaños de clave de 128 y 256 bits, con un nonce de 96 bits, AES-EAX con algunos tamaños de clave y XChaCha20Poly1305. Tienen espacios de claves inconexos, pero todos proporcionan las mismas funciones criptográficas\(\mathrm{Enc}\) y \(\mathrm{Dec}\). (En este debate formal, no vemos un propósito en la contracción de diferentes tamaños de claves de AES-GCM, pero, por supuesto, se podría hacerlo).

Cómo definir primitivas

La manera habitual de pensar en las primitivas es, primero, definir las propiedades de las funciones criptográficas y, luego, considerar a las primitivas como esos algoritmos.

Por ejemplo, para AEAD, diríamos que \(\mathrm{Dec}_k(\mathrm{Enc}_k(m, a), a) = m\) "siempre" está satisfecho (excepto, p.ej., si el texto simple \(m\) es demasiado largo). Además, tenemos propiedades de seguridad; por ejemplo, para una clave aleatoria, la encriptación es semánticamente segura.

Por lo tanto, el tipo primitivo de AEAD es, simplemente, el conjunto de todos los algoritmos criptográficos que cumplen con estas propiedades. En otras palabras, en la práctica, cuando definimos una primitiva específica, lo definimos en función de las propiedades. No damos una lista de algoritmos, como sugiere la definición.

Interfaces

En Tink, una interfaz otorga acceso a una primitiva, en el sentido de que permite procesar un elemento del espacio de salida desde el espacio de entrada. Por ejemplo, considera usar la interfaz AEAD en Java:

public interface Aead {
  byte[] encrypt(byte[] plaintext, byte[] associated_data) throws GeneralSecurityException;
  byte[] decrypt(byte[] ciphertext, byte[] associated_data) throws GeneralSecurityException;
}

Ten en cuenta que no otorgamos acceso a la aleatoriedad. En cambio, permitimos que el usuario proporcione elementos del espacio de entrada. No permitir el acceso a la aleatorización es, por supuesto, intencional.1

Tink a veces ofrece interfaces múltiples para una sola primitiva. Esto puede ser muy útil, ya que los requisitos a veces pueden diferir. Sin embargo, hacer esto tiene un precio: en general, cuantas más interfaces ofrezca una, menor será la interoperabilidad. Por ejemplo, imagina que alguien escribe una biblioteca basada en Tink que requiere que el usuario pase un objeto Aead (para encriptar algo internamente). Si Tink ofrece demasiadas interfaces diferentes para la primitiva de \(\mathrm{AEAD}\) , es probable que el usuario no tenga una instancia lista que funcione para la clave que seleccionó el usuario y la biblioteca al mismo tiempo. Por lo tanto, agregar más interfaces es un problema.


  1. Los algoritmos de cifrado AEAD tienen la propiedad de que son seguros contra ataques de texto cifrado elegidos, lo que se garantiza solo si no se puede reutilizar el nonce. La interfaz de Aead en Tink está diseñada para evitar la reutilización del nonce: el usuario no puede proporcionar un nonce como entrada para la encriptación. En cambio, se genera un nonce nuevo de forma aleatoria para cada operación de encriptación.