Práctica de Diffie-Hellman

Motivación

Hemos visto en terminos generales el funcionamiento del protocolo y pequeñas alteraciones que pueden hacerse para mejorar su seguridad. Vimos también las expresiones matemáticas, así como demostraciones representativas de las operaciones y pasos en el algoritmo. Llegó el momento de ver el procedimiento en acción. A continuación, una implementación en Python del procedimiento para el intercambio de llaves.

Primeros conceptos

Definiremos distintas clases representativas de los agentes y aspectos más importantes del escenario. Una clase con la que instanciar a Alice y a Bob. Seguiremos el procedimiento planteado en la ilustración con las especificaciones matemáticas de la teoría.

Intercambio de llaves

El proceso de encripción debe ocurrir entre dos partes o dos puntos terminales en la conexión. De forma que podemos tomar esta como nuestra medida más general y asignarle atributos y métodos según los necesitamos.

# La clase Partido tendría dos instancias y guarda los secretos propios.
# La información disponible al publico podemos guardarla en variables globales.
class Partido(object):
    # Podemos inicializar cada partido con una llave privada.
    def __init__(self, private_key, important_message):
        # Definimos la llave privada como una variable protegida
        self.__private_key = private_key
        
        #Definimos el mensaje que Alice va a querer enviar a bob.
        self.__message = important_message
        
        # Por lo pronto no hemos establecido el secret compartido.
        self.__shared_key = None
    
    # A un partido podemos pedirle su clave pública dados un generador g y un módulo p.
    def calculate_public_key(self, g, p):
        # P_A = g^a mod p ó
        # P_B = g^b mod p
        return(g ** self.__private_key % p)
    
    # Si conocemos la clave pública del otro partido podemos conocer el secreto compartido.
    def assign_shared_secret(self, others_public_key, p):
        # S = B^a mod p ó
        # S = A^b mod p
        self.__shared_key = others_public_key ** self.__private_key % p
    
    # Si tenemos un secreto compartido, podemos cifrar y entregar el mensaje cifrado.
    # Si aún no tenemos un secreto compartido, el mensaje cifrado es vacío.
    # Este cifrado es simple y potencialmente vulnerable, pero un cifrado robusto no es nuestro enfoque del momento,
    # por lo pronto, solo interesa que el cifrado es simétrico.
    def give_cypher(self):
        outward_cypher = ""
        if self.__shared_key:
            for char in self.__message:
                outward_cypher += chr(ord(char)+self.__shared_key)
        return(outward_cypher)
    
    # Dado un cifrado público, probamos descifrarlo con el secreto compartido.
    def decypher(self, inward_cypher):
        message = ""
        for char in inward_cypher:
            message += chr(ord(char)-self.__shared_key)
        return(message)

Recordemos de programación orientada a objetos que una clase es analoga a un molde, donde esta define las características que puede tener una instancia de esta (objeto). Definimos ahora las instancias Alice y Bob.

# Definimos dos instancias de Partido, Alice y Bob.
# Por ejemplo, podemos usar a = 5 y b = 7
Alice = Partido(int(input("Ingrese una llave privada, a, para Alice: ")),
               "All the best people are.")
Bob = Partido(int(input("Ingrese una llave privada, b, para Bob: ")),
             "Have I gone mad?")

# Note que Alice y Bob tienen variables protegidas.
# Pedirle a Python explicitamente un resultado como Alice.__private_key falla.
try:
  print(Alice.__private_key)
except:
  print("Solo Alice puede saber su clave privada.")
Ingrese una llave privada, a, para Alice: 5
Ingrese una llave privada, b, para Bob: 7
Solo Alice puede saber su clave privada.

Ya que definimos los dos partidos entre los cuales debemos establecer un canal de comunicación seguro, podemos asignar variables públicas al problema. Específicamente, \(g\) y \(p\). Una vez tenemos estos valores establecidos, podemos pedirle a las partes que concedan su clave pública, \(g^{a} \mod p\) y \(g^{b} \mod p\).

# Recordemos que p debe ser un primo muy grande y g un entero menor a p.
# p > a,b
# Utilicemos, por lo pronto, p = 1001 y g = 9.
p = 1001
g = 9

# Le pedimos ahora a las partes sus claves públicas
P_A = Alice.calculate_public_key(g, p)
P_B = Bob.calculate_public_key(g, p)

print(f"Las claves públicas de Alice y Bob son {P_A} y {P_B}, respectivamente.")
Las claves públicas de Alice y Bob son 991 y 191, respectivamente.

Una vez establecidas la claves públicas, puede cada partido calcular un secreto, evaluaremos si el secreto es el mismo para ambos, si pueden desencriptar el mensaje cifrado del otro.

# Cada partido calcula el secreto compartido a partir de la clave pública del otro.
Alice.assign_shared_secret(P_B, p)
Bob.assign_shared_secret(P_A, p)

# Liberamos los mensajes de cada partido, cifrados. Estos son de acceso público.
M_A = Alice.give_cypher()
M_B = Bob.give_cypher()

print(f"El mensaje cifrado de Alice es: {M_A}")
print(f"El mensaje cifrado de Bob es: {M_B}")
El mensaje cifrado de Alice es: РыыϿѓчфϿсфђѓϿяфюяыфϿрёфЍ
El mensaje cifrado de Bob es: ЧрѕфϿШϿцюэфϿьруО

¡Cada partido toma ahora el mensaje cifrado del otro y lo publica! Este usualmente no es el caso, pero lo haremos ahora para validar que el sistema de encripción funciona.

# Alice decifra el mensaje de Bob
print(f"Alice dice que el mensaje de Bob es: '{Alice.decypher(M_B)}'")

# Bob decifra el mensaje de Alice
print(f"Bob dice que el mensaje de Alice es: '{Bob.decypher(M_A)}'")
Alice dice que el mensaje de Bob es: 'Have I gone mad?'
Bob dice que el mensaje de Alice es: 'All the best people are.'

¡Enhoabuena! Alice y Bob lograron intercambiar mensajes sin publicarlos y sin haberse tenido que reunir para establecer una clave simétrica. Confirmamos, dadas las condiciones, la seguridad en la implementación del protocolo de intercambio de llaves.

Reto adicional: ¿Como implementamos autenticación en este esquema? Piense en pares de llaves y en encriptación asimétrica.