Firmado Digital de Mensajes con OpenSSL
RATAS DE BIBLIOTECA
Firmado Digital de Mensajes con OpenSSL
2024-11-27
Por
Don Bit0

En el artículo anterior vimos como usar algoritmos de cifrado asimétricos en general. En esta ocasión vamos a ver como usar esos algoritmos asimétricos para ofrecer servicios reales como el firmado de mensajes. Esto lo podemos hacer utilizando las funciones que ya conocemos, pero OpenSSL nos ofrece funciones alternativas que hacen este proceso más sencillo.

Pero primero vamos a introducir el concepto de firmado digital. El firmado digital de un mensaje es un proceso por el cual se pretende asegurar que el mensaje que hemos recibido proviene de una determinada persona y además no ha sido modificado por una tercera parte.

Con todo lo que sabemos deberías ser capaz de implementar una sencilla aplicación de firmado digital, sin embargo, OpenSSL nos ofrece algunas funciones adicionales que hacen la implementación de una aplicación de cifrado muy sencilla.

Firmado de Mensajes

El proceso de firmado de un mensaje consiste en calcular un hash del mensaje y, a continuación, cifrar ese hash con la clave privada del usuario que lo firma. De esta forma se cumple que:

  • El hash original solo se puede recuperar usando la clave pública del firmante. Lo que significa que la firma fue, necesariamente, cifrada usando su clave privada que solo el sabe.
  • Una vez recuperado el hash que se almacena en la firma, el receptor del mensaje puede calcular el hash del mensaje recibido y compararlo con el que estaba cifrado en la firma, para verificar que el mensaje no ha sido modificado.

Veamos como funcionaría esto usando la utilidad openssl de la línea de comandos. Lo primero que vamos a hacer es generar dos claves para tres usuarios ficticios: Berto y Ali (o Bob y Alice si lo prefieres):

$ openssl genrsa -out berto.pem 2048
$ openssl genrsa -out ali.pem 2048

Ahora vamos a extraer las claves públicas que estarán disponibles para todos los usuarios de nuestros ejemplos:

$ openssl rsa -in berto.pem  -pubout -out berto_publica.pem
$ openssl rsa -in ali.pem  -pubout -out ali_publica.pem

Ahora imaginemos que Berto quiere enviar una factura a Ali y quiere que Ali pueda comprobar que la factura proviene de verdad de Berto y que además nadie la ha modificado en el camino. Berto haría algo como esto:

$  openssl dgst -sha256 -sign berto.pem -out firma-factura.txt factura.txt

Esto genera un fichero binario, el cual podemos convertir en texto para añadirlo al mail o incluirlo al final de la factura. Para ello Berto puede usar el siguiente comando:

$ openssl base64 -in firma-factura.txt -out firma-ascii-factura.txt

Llegados a este punto, Berto enviaría la factura factura.txt junto con la firma-ascii-factura.txt. Cuando Ali recibe la factura, puede verla directamente, la factura no está cifrada, pero tendrá que verificar que la firma digital incluida es correcta. Para ello haría algo como:

$ openssl base64 -d -in fimra-ascii-factura.txt -out /tmp/firma.txt
$ openssl dgst -sha256 -verify berto_publica.pem -signature /tmp/firma.txt factura.txt
Verified OK

Imaginemos que ahora Marta, sigilosamente saca la factura del correo de Alicia, la modifica haciendo que la cantidad sea mayor y cambia el número de cuenta en el que debe pagarla. Sin embargo, no puede generar una firma que se verifique con la clave pública de Berto, puesto que no conoce su clave privada. De esta forma, la firma original no se corresponde con el fichero recibido y Alicia puede estar segura de que la factura ha sido modificada.

Los comandos de arriba realmente hacen las siguientes operaciones:

  • Calcular el hash del documento factura.txt usando el algoritmo sha256
  • Cifrar el hash usando la clave privada del firmante.

Implementando nuestro propio programa de firmado

Como os adelantábamos al principio, OpenSSL nos ofrece unas funciones que facilitan el firmado de mensajes y, sabéis que?, que funcionan igual que el resto de funciones que hemos estado usando hasta ahora.

Este es el programa que implementa el firmado digital de cualquier fichero. Veamos primero todo el código y luego prestemos atención a las partes interesantes:

#include <openssl/evp.h>
#include <openssl/err.h>
#include <openssl/decoder.h>

#include <stdio.h>
#include <stdlib.h>

int main (int argc, char *argv[]) {
  OSSL_LIB_CTX *libctx = NULL;
  
  /* Manejo de Claves Claves */
  OSSL_DECODER_CTX *dctx = NULL;
  EVP_PKEY_CTX     *pkctx = NULL;
  EVP_PKEY         *priv_key = NULL;
  
  /* Función Hash*/
  const EVP_MD      *md;
  EVP_MD_CTX        *mdctx;

  if (argc != 4) {
    fprintf (stderr, "Usage:¨\n%s private.pem hash-algo input_file\n",
         argv[0]);
    exit (1);
  }
  libctx = NULL; /* Usa el contexto de librería por defecto */

  /* PASO 1: Adquirimos las claves para el firmado */
  /* --------------------------------------------- */
  dctx = OSSL_DECODER_CTX_new_for_pkey (&priv_key, NULL, NULL, NULL,
                    EVP_PKEY_KEYPAIR,libctx, NULL);

  /* Lee par de claves */
  FILE *f = fopen (argv[1], "rb");
  OSSL_DECODER_from_fp(dctx, f);
  fclose (f);
  OSSL_DECODER_CTX_free(dctx);
  /* ------------------------------------- */
  
  /* PASO 2: Creamos función Hash */
  /* ---------------------------- */
  if ((md = EVP_get_digestbyname (argv[2])) == NULL) {
        ERR_print_errors_fp (stderr);
  }
  mdctx = EVP_MD_CTX_create ();
  /* ------------------------------------- */

  /* PASO 3. Firmado del mensaje  */
  /* ---------------------------- */
  if (EVP_SignInit (mdctx, md) <= 0) {
    ERR_print_errors_fp (stderr);
    exit (1);
  }

  f = fopen (argv[3], "rb");
  unsigned char buf[1024];
  int           size;
  while (!feof (f)) {
    if ((size = fread (buf, 1, 1024, f)) <= 0) break;
    EVP_SignUpdate (mdctx, buf, size);
  }
  unsigned char sig[1024];
  unsigned int  sig_len;
    
  if (EVP_SignFinal(mdctx, sig, &sig_len, priv_key) <=0) {
    ERR_print_errors_fp (stderr);
    exit (1);
  }
  /* ------------------------------------- */
  
  /* PASO 4 Imprime firma en consolal */
  /* ------------------------------------- */
  fwrite (sig, 1 , sig_len, stdout);
}

Como podéis ver el código es muy sencillo y lo podemos dividir en cuatro pasos:

  • La primera parte lee la clave privada del fichero que pasamos como primer parámetro. Este es el mismo código que usamos en el artículo sobre cifrado asimétrico.
  • La segunda parte configura el algoritmo de hash que queremos usar y que pasamos como segundo parámetro. Este código también lo vimos en el artículo sobre hashing.
  • El tercer paso es el cifrado del mensaje. Este código es nuevo, pero debería resultaros muy familiar. Lo veremos en detalle en un segundo.
  • El cuarto paso consiste en volcar la forma en la consola al estilo openssl, es decir, los datos binarios para procesarlos con herramientas externas si fuera necesario.

Utilizando los ficheros que usamos en la introducción con la utilidad openssl podemos verificar que nuestro programa funciona correctamente, de la siguiente forma:

$ gcc -O0 -g -o firmado firmado.c -lcrypto -lssl
$ ./firmado berto.pem sha256 factura.txt | xxd -p
94dbb4f53a85bb2e33d8c912f269767f792346cd00ce8daa29d8434373e5
aad9295680a58953a79935d7d28b849ac7efd1cefc8524fb50405d21f835
edccdeca858de8594e52cd0805ef37804ca7b2c77cacc4e7ce83e67e5ae9
b0c91b3974aefefe3ecc162d2da9343f622bfd3e648eaf29ce7a8e833e52
e132350d01b5945fb56037555f7052bc14ada4d1af9ed0267cdc532c50c3
5c907f2394adead8ed2a2834bfe008607c254a8c10a0b4dbfe1d1a9f68ba
6b28d74dadf22d8bdee35f9c5c1f21308d0b56ddac0f1596df2b28411f5a
bedf4264a0ee7814954b1ed2bfbdff13b45376fd3fe37de670a32acbd7d0
385f529d5223b1ecdff968833a766048
$ openssl dgst -sha256 -sign berto.pem factura.txt | xxd -p
94dbb4f53a85bb2e33d8c912f269767f792346cd00ce8daa29d8434373e5
aad9295680a58953a79935d7d28b849ac7efd1cefc8524fb50405d21f835
edccdeca858de8594e52cd0805ef37804ca7b2c77cacc4e7ce83e67e5ae9
b0c91b3974aefefe3ecc162d2da9343f622bfd3e648eaf29ce7a8e833e52
e132350d01b5945fb56037555f7052bc14ada4d1af9ed0267cdc532c50c3
5c907f2394adead8ed2a2834bfe008607c254a8c10a0b4dbfe1d1a9f68ba
6b28d74dadf22d8bdee35f9c5c1f21308d0b56ddac0f1596df2b28411f5a
bedf4264a0ee7814954b1ed2bfbdff13b45376fd3fe37de670a32acbd7d0
385f529d5223b1ecdff968833a766048

Como podéis ver el contenido del fichero de firma (antes de convertirlo a base64) es el mismo que el generado por nuestro programa. Echemos un ojo al código para cerrar esta sección:

Proceso de firmado

Como sucede con la mayoría de funciones EVP_ que ya hemos visto, el proceso de firmado, funciona básicamente como el proceso de generación de un hash. De hecho, eso es lo que el programa hace, excepto por la última llamada. Veamos como funcionan las tres funciones que nos interesa :

  EVP_get_digestbyname (argv[2]);
  mdctx = EVP_MD_CTX_create ();
  
  EVP_SignInit (mdctx, md);
   ...

Como es típico con el interfaz EVP_, lo primero que tenemos que hacer es inicializar el contexto. En este caso, la inicialización es exactamente la misma que usamos para generar hashes, pero utilizando EVP_SignInit en lugar de EVP_DigestInit. El parámetro más importante para la inicialización es la función hash y el contexto para calcularlo que obtenemos con las funciones EVP_get_digestbyname y EVP_MD_CTX_create.

Una vez que hayamos inicializado nuestro contexto, solamente tendremos que pasar el contenido del fichero que queremos firmar usando la función EVP_SignUpdate, la cual funciona exactamente igual que EVP_DigestUpdate. El fragmento de código siguiente, nos muestra una posible forma de leer el contenido del fichero a cifrar e introducir los datos para el cálculo del hash.

 f = fopen (argv[3], "rb");
  unsigned char buf[1024];
  int           size;
  while (!feof (f)) {
    if ((size = fread (buf, 1, 1024, f)) <= 0) break;
    EVP_SignUpdate (mdctx, buf, size);
  }

Una vez que hayamos introducido todos los datos tenemos que llamar a EVP_SignFinal la cual requiere un parámetro más que EVP_DigestFinal, ese parámetro no es otro que la clave privada que vamos a usar para la firma.

  unsigned char sig[1024];
  unsigned int  sig_len;
  
  EVP_SignFinal(mdctx, sig, &sig_len, priv_key)

Nótese que la firma final se realiza con el algoritmo asociado a la clave. Si hemos utilizado una clave RSA 2048, la firma tendrá un tamaño de 256 bytes (2048 bits), y el tamaño del hash tendrá que ser menor que esos 2048 bits si no queremos perder información. Recordad que RSA genera bloques cifrados del tamaño de la calve usada.

Como podéis ver, las funciones EVP_Sign* resultan muy convenientes para firmar mensajes, haciendo que el proceso resulte muy sencillo.

Verificando una Firma Digital

De la misma forma que OpenSSL nos ofrece funciones para el firmado de mensajes también nos ofrece funciones para verificar si una determinada firma digital es correcta o no. Como os podéis imaginar disponemos de un interfaz del tipo EVP_ y en este caso las tres funciones que debemos usar tienen como prefijo EVP_Verigy.

Aquí tenéis el código de un sencillo programa capaz de verificar si una determinada firma digital es correcta. Podéis usar este programa para comprobar la firma generada usando la utilidad de línea de comando openssl al principio de este artículo.

Aquí tenéis el código.

#include <openssl/evp.h>
#include <openssl/err.h>
#include <openssl/decoder.h>
#include <stdio.h>
#include <stdlib.h>


int main (int argc, char *argv[]) {
  OSSL_LIB_CTX *libctx = NULL;
  
  /* Manejo de Claves */
  OSSL_DECODER_CTX *dctx = NULL;
  EVP_PKEY_CTX     *pkctx = NULL;
  EVP_PKEY         *pub_key = NULL;
  
  /* Función Hash*/
  const EVP_MD      *md;
  EVP_MD_CTX        *mdctx;

  if (argc != 5) {
    fprintf (stderr, "Usage:¨\n%s public.pem hash-algo input_file firma\n",
         argv[0]);
    exit (1);
  }
  libctx = NULL; /* Usa el contexto de librería por defecto */

  /* PASO 1: Adquirimos las claves para el firmado */
  dctx = OSSL_DECODER_CTX_new_for_pkey (&pub_key, NULL, NULL, NULL,
                    EVP_PKEY_PUBLIC_KEY,libctx, NULL);

  /* Lee clave pública */
  FILE *f = fopen (argv[1], "rb");
  OSSL_DECODER_from_fp(dctx, f);
  fclose (f);
  OSSL_DECODER_CTX_free(dctx);

  /* PASO 2: Lee firma */
  int           size;
  int           esig_len = 0;
  unsigned char esig[1024];
  
  f = fopen (argv[4], "rb");
  if ((esig_len = fread (esig, 1, 1024, f)) < 0){
    fprintf (stderr, "Error leyendo firma digital\n");
  }
  fclose (f);

  /* PASO 3: creamos función Hash */
  if ((md = EVP_get_digestbyname (argv[2])) == NULL) {
        ERR_print_errors_fp (stderr);
  }

  mdctx = EVP_MD_CTX_create ();

  /* PASO 4: Verificamos la firma*/
  if (EVP_VerifyInit (mdctx, md) <= 0) {
    ERR_print_errors_fp (stderr);
    exit (1);
  }

  f = fopen (argv[3], "rb");
  unsigned char buf[1024];

  while (!feof (f)) {
    if ((size = fread (buf, 1, 1024, f)) <= 0) break;
    EVP_VerifyUpdate (mdctx, buf, size);
  }
  unsigned char sig[1024];
  unsigned int  sig_len;
    
  if (EVP_VerifyFinal(mdctx, esig, esig_len, pub_key) <=0) {
    printf ("Firma incorrecta\n");
    exit (1);
  }
  printf ("Verificación OK\n");
}

Como podéis ver, el programa es exactamente igual al que hemos creado para generar firmas digitales con dos salvedades.

  • La primera es que tenemos que leer la forma en memoria para poder comprobar si es correcta
  • La segunda es que la función EVP_VerificaFinal recibe unos parámetros bastante diferentes a la función EVP_SignFinal.

Sobre la primera diferencia no hay mucho que decir, simplemente leemos el fichero que recibimos como parámetro en memoria. Esa es la firma digital en formato binario generada por nuestra aplicación o por openssl.

Respecto de la segunda, EVP_VerifyFinal recibe como parámetro la firma que queremos verificar y la clave pública de quien dice haber firmado el mensaje y nos devolverá 1 si la firma es correcta y 0 sino. Recordad que EVP_SignFinal recibía como parámetro un buffer en el que se almacenaría la clave, una variable para almacenar si tamaño y la clave privada para generar la firma.

Ahora podéis compilar este programa y comprobar que es capaz de verificar firmas generadas con openssl dgst.

$ gcc -O0 -g -o verificador verificador.c -lcrypto -lssl
$ openssl dgst -sha256 -sign berto.pem -out firma1.sign factura.txt
$ openssl dgst -sha256 -verify berto_publica.pem -signature firma1.sign factura.txt
Verified OK
$ ./verificador berto_publica.pem sha256 factura.txt firma1.sign
Verificación OK
$ echo "FICHERO MODIFICADO" >> factura.txt
$ openssl dgst -sha256 -verify berto_publica.pem -signature firma1.sign factura.txt
Verification failure
$ ./verificador berto_publica.pem sha256 factura.txt firma1.sign
Firma incorrecta

Conclusiones

En este artículo hemos visto como utilizar OpenSSL para generar firmas digitales de cualquier fichero que queremos usando las funciones especiales que ofrece para estos menesteres. De la misma forma, hemos aprendido como verificar si una firma digital es válida y hemos escrito dos sencillos programas para generar firmas y verificarlas que funcionan perfectamente con la utilidad openssl. Seguid sintonizados, en el próximo número vamos a continuar explorando el extenso API que ofrece OpenSSL

Header Image Credits: energepic.com

SOBRE Don Bit0
No os podemos contar mucho sobre Don Bit0. Es un tipo misterioso que de vez en cuando colabora con nosotros y luego, simplemente se desvanece. Como os podéis imaginar por su nick, Don Bit0, está cómodo en el bajo nivel, entre bits, y cerquita del HW que lo mantiene calentito.

 
Tu publicidad aquí :)