Criptografía para la comunicación y autenticación con el implante

Introducción

Es deseable que la comunicación con el implante esté cifrada para evitar que pueda detectarse y/o analizarse, así como autenticada para identificar unívocamente a los agentes, de modo que unos no puedan hacerse pasar por otros. Esto último es importante para el caso en que alguien llegara a tomar control de alguno de nuestros implantes y su clave privada.

Criptografía simétrica

El cifrado simétrico es un método que nos permite encriptar y desencriptar un mensaje utilizando una sola clave secreta, que debe ser conocida tanto por (y solo por) el emisor y el receptor del mismo. En nuestro caso utilizaremos AES en modo Output Feedback (OFB) sin padding.

Implementación

class SymmetricCipher:
    def __init__(self, key: bytes) -> bytes:
        self._key = key
    
    def encrypt_msg(self, msg: bytes) -> bytes:
        """
            Cifra el mensaje en formato bytes
        """
        iv = get_random_bytes(AES.block_size)
        cipher = AES.new(self._key, AES.MODE_OFB, iv)
        padded_msg = msg.ljust(len(msg) + ((-len(msg)) % AES.block_size), b'\x00')
        return iv + cipher.encrypt(padded_msg)[:len(msg)]
    
    def decrypt_msg(self, msg: bytes) -> bytes:
        """
            Descifra el mensaje en formato bytes
        """
        iv, msg = msg[:AES.block_size], msg[AES.block_size:]
        cipher = AES.new(self._key, AES.MODE_OFB, iv)
        padded_msg = msg.ljust(len(msg) + ((-len(msg)) % AES.block_size), b'\x00')
        return cipher.decrypt(padded_msg)[:len(msg)]
    
    def sign_msg(self, msg: bytes) -> bytes:
        """
            Firma el mensaje en formato bytes
        """
        return hmac.digest(self._key, msg, 'sha1') + msg

    def verify_msg(self, msg: bytes) -> Optional[bytes]:
        """
            Verifica la firma de un mensaje
        """
        sig, msg = msg[:20], msg[20:]
        verify_sig = hmac.digest(self._key, msg, 'sha1')
        if hmac.compare_digest(sig, verify_sig):
            return msg
        else:
            return None
    
    def encrypt_sign_msg(self, msg: bytes) -> bytes:
        """
            Cifra y firma un mensaje
        """
        return self.sign_msg(self.encrypt_msg(msg))
    
    def verify_decrypt_msg(self, msg: bytes) -> Optional[bytes]:
        """
            Verifica la firma y luego decifra el mensaje. 
            En caso de no poder verificar la firma devuelve None
        """
        verified_msg = self.verify_msg(msg)
        if verified_msg is not None:
            return self.decrypt_msg(verified_msg)
        else:
            return None

Ejemplo de uso

from sym import SymmetricCipher
import random

# La clave debe tener exactamente 16 bytes
cipher = SymmetricCipher(random.randbytes(16))

msg = b"Pucara"
encrypted_msg = cipher.encrypt_msg(msg)
encrypted_signed_msg = cipher.sign_msg(encrypted_msg)

# El mensaje verificado y descifrado debe ser igual al original
assert msg == cipher.verify_decrypt_msg(encrypted_signed_msg)

alt_encrypted_signed_msg = b'\x00' + encrypted_signed_msg[1:]

# Al modificar el mensaje la verificación falla
assert cipher.verify_decrypt_msg(alt_encrypted_signed_msg) is None

Criptografía asimétrica

Este tipo de cifrado utiliza pares de claves, una clave pública (la cual puede ser conocida por terceros) y una clave privada (que debe ser mantenida en reserva por el propietario de las claves). En nuestro caso utilizaremos un criptosistema de curvas elípticas (ECC). Las propiedades del mismo nos permitirán, a partir del conocimiento de la propia clave privada y la clave pública de nuestro par, deducir una clave compartida que luego utilizaremos como clave simétrica en la comunicación. El protocolo que nos permitirá hacerlo está descripto en la siguiente sección.

Arquitectura

El cifrado entre agente y listener cuenta con dos capas:

La capa 1 es un cifrado simétrico con autenticación por listener, es decir, todos los agentes que se comuniquen con un listener determinado utilizaran la misma clave. Esto aún tiene la desventaja de que al tomar control de un agente, uno se haría de la clave y podría si quisiera impersonar a otros agentes. Su utilidad es mayormente ocultar todo tipo de formato propio de los protocolos que utilizaremos en capas superiores.

La capa 2 es un poco más compleja, también consiste de un cifrado simétrico con autenticación donde la clave no es global, sino que se negocia por cada agente de forma segura utilizando el protocolo Diffie-Hellman, esto como ya se mencionó, impide que el comprometimiento de un agente afecte al sistema entero. Para más detalles de la implementación ver la sección "Diffie-Hellman"

Intercambio de claves

Para realizar el intercambio de claves entre agente y listener utilizaremos un protocolo llamado "Diffie-Hellman". Este permite, a partir del intercambio de claves públicas de un criptosistema del tipo de "Curvas elípticas" (ECC), ponerse de acuerdo sobre una clave de criptografía simétrica.

El protocolo se describe a continuación:

Pros:

  • No es necesario compartir claves entre listeners (ni por supuesto, agentes).

  • Provee un canal cifrado y autenticado por agente.

  • La clave, además de cifrar, permite identificar unívocamente a cada agente

Cons:

  • No nos protege contra MITM. Para solucionarlo se puede hacer pinning de las claves públicas.

Implementación

class KeyExchange:
    def __init__(self, private_key_der: Optional[bytes] = None):
        """
            Puede recibir una clave privada como input. 
            En caso de no recibir una, la genera utilizando la
            SigningKey y curva NIST256p.
        """
        if private_key_der is not None:
            self.private_key = SigningKey.from_der(private_key_der)
        else:
            self.private_key = SigningKey.generate(curve=NIST256p)
    
    def get_shared_key(self, public_key_der: bytes) -> bytes:
        """
            Utilizacion de ECDH para fabricar la clave compartida.
        """
        return ECDH(
            curve=NIST256p,
            private_key=self.private_key,
            public_key=VerifyingKey.from_der(public_key_der)
        ).generate_sharedsecret_bytes()
    
    def get_public_key(self) -> bytes:
        """
        Devuelva la clave publica del keyExchange en el format "der"
        """
        public_key: VerifyingKey = self.private_key.get_verifying_key()
        return public_key.to_der()
    
    def get_private_key(self) -> bytes:
        """
        Devuelva la clave privada del keyExchange en el format "der"
        """
        return self.private_key.to_der()

Ejemplo de uso

from ecdsa.curves import NIST192p, NIST256p
from dh import KeyExchange
from ecdsa import SigningKey

# Generamos par clave publica / clave privada del cliente
client_privkey = SigningKey.generate(curve=NIST256p).to_der()
client_ke = KeyExchange(client_privkey)
client_pubkey = client_ke.get_public_key()

# Generamos par clave publica / clave privada del servidor
server_privkey = SigningKey.generate(curve=NIST256p).to_der()
server_ke = KeyExchange(server_privkey)
server_pubkey = server_ke.get_public_key()

# Generamos clave compartida a partir 
# de la clave privade del cliente y la clave publica del servidor
# esta es la información que posee el cliente luego de recibir
# la clave publica del servidor
client_shared_key = client_ke.get_shared_key(server_pubkey)

# Generamos clave compartida de forma analoga para el servidor
server_shared_key = server_ke.get_shared_key(client_pubkey)

assert client_shared_key == server_shared_key

Nota sobre curvas elípticas

Dado que el criptosistema utilizado para realizar el intercambio de claves es ECC, podemos aprovechar otras propiedades del mismo para más adelante implementar funciones que pueden resultar interesantes. Una de ellas es la derivación jerárquica (en árbol) de claves (del estilo que se utiliza en criptomonedas como Bitcoin). Esta nos permitiría, por ejemplo, que un agente pueda propagarse por una red produciendo otros agentes con claves hijas, de modo que, sin necesidad de hacer pinning de las mismas podamos en el listener determinar que están relacionadas con este agente "padre" y autenticarlos automáticamente. Incluso el comprometimiento de una de estas claves no implica el de su padre ni sus hermanas, lo cual también incrementa la dificultad de hacer análisis sobre nuestras actividades.

Última actualización