Ir al contenido principal

Criptografía (CCLXXXVII): Firma digital y cifrado ElGamal en python

Continúo poniendo scripts de programación en python para automatizar tareas que tengan relación con la criptografía.

En este post incluyo un script en python para  implementar la firma digital y el cifrado con el esquema de firma digital y el esquema de cifrado ElGamal.

Antes de poner el script, recordar que en este post referido a RSA, decía que los esquemas modernos de cifrado utilizan conjuntamente los conceptos de clave pública y clave privada, es decir, el mensaje o archivo completo se cifra con un algoritmo de clave privada o simétrica (por ejemplo, AES) y luego la clave empleada en ese cifrado (clave de sesión), a su vez, se cifra con un algoritmo de clave pública o asimétrica (por ejemplo, RSA):

Por tanto, como se ve en la figura anterior, el texto en claro se cifra mediante una clave de sesión, un número correspondiente a cada mensaje particular, y la clave de sesión se cifra utilizando la clave pública del receptor, de tal manera que sólo el receptor puede descifrar la clave de sesión mediante su clave privada (criptografía asimétrica) y posteriormente descifrar el criptograma o texto en claro cifrado mediante la clave de sesión (criptografía simétrica).

De esta forma, sólo se podrá obtener la clave de sesión (clave simétricasi se está en posesión de la clave privada, y, por consiguiente, si no se tiene esta última no se puede descifrar el criptograma o texto en claro cifrado.

Este esquema de cifrado se puede utilizar para mantener la confidencialidad de los mensajes y los archivos, sin tardar demasiado ni consumir demasiados recursos informáticos, el esquema de firma digital se puede utilizar para firmar digitalmente los mensajes y los archivos, es decir, para garantizar al receptor que el emisor es realmente quien los ha enviado (autenticidad: el emisor es quien dice ser) y para comprobar que no han sido interceptados y alterados por terceros (integridad); con la característica adicional de no repudiotanto en origen (el emisor no puede negar que creó y envió esos textos en claro) como en destino (el receptor no puede negar que los recibió y recibió exactamente esos textos en claro).

El script que pongo a continuación implementa las operaciones de firma digital del texto en claro y de cifrado de la clave de sesión mediante ElGamal, y el cifrado del texto en claro mediante el algoritmo AES de criptografía simétrica en modo de operación CBC, es decir, las operaciones que se realizan en la comunicación desde el lado del emisor.

Previamente he generado, mediante el script que puse en esta entrada, el par de claves ElGamal, publica y privada, para los dos usuarios participantes en la comunicación, emisor y receptor: clave pública emisorclave privada emisorclave pública receptorclave privada receptor.

Asimismo, para que el script funcione se necesita importar el siguiente módulo en el programa principal, que se encargará de hacer de forma eficiente las operaciones de exponenciación modular.

#!/usr/bin/env python
# -*- coding: utf-8 -*-

# ALGORITMO DE EXPONENCIACIÓN MODULAR RÁPIDA:
#
# Algoritmo utilizado en operaciones de exponenciación modular
# con números muy grandes con objeto de hacerlas de forma
# más eficiente.
#
# http://mikelgarcialarragan.blogspot.com/

def exp_modular_rapida(b, e, m):
    c = 1
    e = bin(e)[2:]

    for i in range(0, len(e)):
        c = pow(c, 2, m)
        if e[i]=="1":
            c = c*b%m

    return c

El script es el siguiente:

Script python para la firma digital y cifrado ElGamal:

#!/usr/bin/env python
# -*- coding: utf-8 -*-

# FIRMA Y CIFRADO ELGAMAL:
#
# Firma digitalmente y cifra utilizando el esquema de firma y de cifrado ElGamal.
#
# http://mikelgarcialarragan.blogspot.com/

import hashlib
import random
import math
from Crypto import Random
from Crypto.Util.Padding import pad
from Crypto.Cipher import AES
import pyasn1.type.univ
from pyasn1.codec.der import decoder
from base64 import b64encode, b64decode
from exponenciacion_modular_rapida import exp_modular_rapida

# CIFRADO AES (CRIPTOGRAFÍA SIMÉTRICA).
def cifrar_aes(texto_claro,clave):
    iv = Random.new().read(AES.block_size)
    print("[+] IV (hexadecimal):", iv.hex())
    cifrar = AES.new(clave, AES.MODE_CBC, iv)
    criptograma = iv + cifrar.encrypt(pad(texto_claro.encode(), AES.block_size))
    return criptograma

# OBTENER DATOS DE LA CLAVE PÚBLICA ELGAMAL
def clave_publica_elgamal(fichero_pem):
    f_clave_publica_elgamal = open(fichero_pem, "r")
    clave_publica = f_clave_publica_elgamal.read()
    clave_publica = clave_publica.replace("-----BEGIN ELGAMAL PUBLIC KEY-----","")
    clave_publica = clave_publica.replace("-----END ELGAMAL PUBLIC KEY-----","")
    datos_clave_publica_elgamal = pyasn1.codec.der.decoder.decode(b64decode(clave_publica.encode('ascii')))
    f_clave_publica_elgamal.close()
    return datos_clave_publica_elgamal

# OBTENER DATOS DE LA CLAVE PRIVADA ELGAMAL
def clave_privada_elgamal(fichero_pem):
    f_clave_privada_elgamal = open(fichero_pem, "r")
    clave_privada =f_clave_privada_elgamal.read()
    clave_privada = clave_privada.replace("-----BEGIN ELGAMAL PRIVATE KEY-----","")
    clave_privada = clave_privada.replace("-----END ELGAMAL PRIVATE KEY-----","")
    datos_clave_privada_elgamal = pyasn1.codec.der.decoder.decode(b64decode(clave_privada.encode('ascii')))
    f_clave_privada_elgamal.close()
    return datos_clave_privada_elgamal

def main():
# MENÚ:
# Se presenta el menú para que se seleccione una opción.
    salir = False
    while not salir:
        print ("")
        print ("*** MENÚ *****************************************")
        print ("1. Firmar digitalmente y cifrar clave de sesión AES.")
        print ("2. Salir.")
        print ("")
        opcion = input("Por favor, seleccione una opción: ")
        if opcion == "1":
            print ("")
            print ("--- FIRMAR DIGITALMENTE Y CIFRAR CLAVE DE SESIÓN:")
            # Firmar digitalmente y cifrar: Se introducen el texto en claro y la clave.
            texto_claro = clave = ""
            while texto_claro == "":
                texto_claro = input('Texto en claro a cifrar: ')
                if texto_claro != "":
                    while clave == "":
                        clave = input('Clave: ')
                        if clave != "":
                            # Se genera la clave de sesión AES con la que se va a cifrar el
                            # texto en claro y que, a su vez, va a ser cifrada con la clave
                            # pública del destinatario, que será descifrada por éste con su
                            # clave privada (criptografía asimétrica) para, a su vez, descifrar
                            # el criptograma (criptografía simétrica).
                            # La clave se genera a partir de la clave introducida por el usuario,
                            # que puede tener cualquier longitud, y de la cual se calcula su
                            # hash SHA-256 (256 bits).
                            # Ese hash será la clave de sesión (32 bytes).
                            clave_sesion = (hashlib.sha256(clave.encode()).digest())
                            print ("[+] Clave de sesión AES generada (hexadecimal):", clave_sesion.hex())
                            criptograma = cifrar_aes(texto_claro,clave_sesion)
                            print ("[+] Texto claro cifrado con la clave de sesión AES (hexadecimal):", criptograma.hex())
                            # Se lee la clave privada del emisor para firmar digitalmente el texto en claro.
                            datos_clave_privada = clave_privada_elgamal("private_emisor.pem")
                            print ("[+] Clave privada del emisor:")
                            print ("    p:", datos_clave_privada[0][0])
                            print ("    g:", datos_clave_privada[0][1])
                            print("     k:", datos_clave_privada[0][2])
                            print ("    a:", datos_clave_privada[0][3])
                            #Se genera aleatoriamente un número primo ('h'), mayor o igual que 1 y menor o igual que p - 1,
                            #coprimo o primo relativo con phi(p)
                            while True:
                                h = random.randint(1, datos_clave_privada[0][0]-1)
                                if math.gcd(h, datos_clave_privada[0][0]-1) == 1:
                                    break
                            print ("[+] Número primo 'h' coprimo con phi(p) generado aleatoriamente:", h)
                            # Se calcula la rúbrica del emisor ('r'), primera parte de la firma digital.
                            r = exp_modular_rapida(datos_clave_privada[0][1], h, int(datos_clave_privada[0][0]))
                            # Se calcula 's', segunda parte de la firma digital.
                            h_M = (hashlib.sha256(texto_claro.encode()).digest())
                            s = ((int(h_M.hex(),16) - datos_clave_privada[0][3] * r) * pow(h,-1,int(datos_clave_privada[0][0])-1))%(datos_clave_privada[0][0]-1)
                            print ("[+] Firma digital: (", r, ",", s, ")")
                            # Se lee la clave pública del receptor para cifrar la clave de sesión AES.
                            datos_clave_publica = clave_publica_elgamal("public_receptor.pem")
                            print ("[+] Clave pública del receptor:")
                            print ("    p:", datos_clave_publica[0][0])
                            print ("    g:", datos_clave_publica[0][1])
                            print("     k:", datos_clave_publica[0][2])
                            b = random.randint(1, (datos_clave_publica[0][0])-1)
                            c1 = exp_modular_rapida(datos_clave_publica[0][1], b,int(datos_clave_publica[0][0]))
                            c2 = (int(clave_sesion.hex(),16) * exp_modular_rapida(datos_clave_publica[0][2], b, int(datos_clave_publica[0][0])))%datos_clave_publica[0][0]
                            print ("[+] Clave de sesión cifrada con la clave pública Elgamal del receptor (hexadecimal): (", hex(c1)[2:], ",", hex(c2)[2:], ")")
                            f_comunicacion = open("comunicacion.txt", "w")
                            f_comunicacion.write((b64encode(str(c1).encode())).decode() + "\n")
                            f_comunicacion.write((b64encode(str(c2).encode())).decode() + "\n")
                            f_comunicacion.write(b64encode(criptograma).decode() + "\n")
                            f_comunicacion.write((b64encode(str(r).encode()).decode()) + "\n")
                            f_comunicacion.write((b64encode(str(s).encode()).decode()))
                            f_comunicacion.close()
                        else:
                            print ("*** ERROR: Por favor, introduzca la clave.")
                else:
                    print ("*** ERROR: Por favor, introduzca el texto en claro a cifrar.")
        elif opcion == "2":
            print ("*** FIN ******************************************")
            salir = True
        else:
            print ("*** ERROR: Opción no válida.")

if __name__ == '__main__':
    main()

Lo ejecuto:

Tras introducirse el mensaje o texto en claro a cifrar, lo primero que hace el script es generar la clave AES, calculando el hash SHA-256 de la clave introducida por el usuario, con la que se cifra el texto en claro y se obtiene el criptograma o texto en claro cifrado (criptografía simétrica).

Lo siguiente que hace el script es leer la clave privada del emisor, almacenada en formato PEM en un fichero, y generar aleatoriamente un número primo ('h'), mayor o igual que 1 y menor o igual que p - 1,  coprimo o primo relativo con ϕ(p)para firmar digitalmente.

Posteriormente, el script lee la clave pública del receptor, almacenada en formato PEM en otro fichero, para cifrar mediante el algoritmo ElGamal (criptografía asimétrica) la clave de sesión AES con la que se ha cifrado el mensaje o texto en claro (criptografía simétrica).

Y, finalmente, para simular el envió de la comunicación, se genera un fichero, comunicación.txt, en el que se almacenan, codificados en base64, lo siguientes datos: el par de valores correspondientes al cifrado de la clave de sesión AES, el criptograma y el par de valores correspondientes a la firma digital. 

De esta forma, cuando el receptor reciba la comunicación, lo primero que se hará será descifrar la clave de sesión AES con su clave privada (criptografía asimétrica) y con ésta descifrar el criptograma (criptografía simétrica), para, posteriormente, verificar la firma digital con la clave pública del emisor (criptografía asimétrica).

En un post posterior pondré el script en python para implementar esto último.

Quizás también te interese:

Comentarios

Entradas populares de este blog

Criptografía (I): cifrado Vigenère y criptoanálisis Kasiski

Hace unos días mi amigo Iñaki Regidor ( @Inaki_Regidor ), a quien dedico esta entrada :), compartió en las redes sociales un post titulado "Criptografía: el arte de esconder mensajes"  publicado en uno de los blogs de EiTB . En ese post se explican ciertos métodos clásicos para cifrar mensajes , entre ellos el cifrado de Vigenère , y , al final del mismo, se propone un reto consistente en descifrar un mensaje , lo que me ha animado a escribir este post sobre el método Kasiski  para atacar un cifrado polialfabético ( conociendo la clave descifrar el mensaje es muy fácil, pero lo que contaré en este post es la forma de hacerlo sin saberla ). El mensaje a descifrar es el siguiente: LNUDVMUYRMUDVLLPXAFZUEFAIOVWVMUOVMUEVMUEZCUDVSYWCIVCFGUCUNYCGALLGRCYTIJTRNNPJQOPJEMZITYLIAYYKRYEFDUDCAMAVRMZEAMBLEXPJCCQIEHPJTYXVNMLAEZTIMUOFRUFC Como ya he dicho el método de Vigenère es un sistema de sustitución polialfabético , lo que significa que, al contrario que en un sistema de

Criptografía (XXIII): cifrado de Hill (I)

En este post me propongo explicar de forma comprensible lo que he entendido sobre el cifrado de Hill , propuesto por el matemático Lester S. Hill , en 1929, y que se basa en emplear una matriz como clave  para cifrar un texto en claro y su inversa para descifrar el criptograma correspondiente . Hay tres cosas que me gustan de la criptografía clásica, además de que considero que ésta es muy didáctica a la hora de comprender los sistemas criptográficos modernos: la primera de ellas es que me "obliga" a repasar conceptos de matemáticas aprendidos hace mucho tiempo y, desgraciadamente, olvidados también hace demasiado tiempo, y, por consiguiente, que, como dice  Dani , amigo y coautor de este blog, me "obliga" a hacer "gimnasia mental"; la segunda es que, en la mayoría de las ocasiones, pueden cifrarse y descifrase los mensajes, e incluso realizarse el criptoanálisis de los criptogramas, sin más que un simple lápiz y papel, es decir, para mi es como un pasat

¿Qué significa el emblema de la profesión informática? (I)

Todas o muchas profesiones tienen un emblema que las representa simbólicamente y en el caso de la  informática: " es el establecido en la resolución de 11 de noviembre de 1977  para las titulaciones universitarias superiores de informática, y  está constituido por una figura representando en su parte central  un  núcleo toroidal de ferrita , atravesado por  hilos de lectura,  escritura e inhibición . El núcleo está rodeado por  dos ramas : una  de  laurel , como símbolo de recompensa, y la otra, de  olivo , como  símbolo de sabiduría. La  corona  será la  de la casa real  española,  y bajo el escudo se inscribirá el acrónimo de la organización. ". Veamos los diferentes elementos tomando como ejemplo el emblema del COIIE/EIIEO (Colegio Oficial de Ingenieros en Informática del País Vasco/ Euskadiko Informatikako Ingeniarien Elkargo Ofiziala ) . Pero no sólo el COIIE/EIIEO adopta el emblema establecido en dicha resolución, sino que éste se adopta también como im