martes, 27 de abril de 2010

[How to] OpenSSL en C/C++

Pues como parece que la documentacion de OpenSSL no es precisamente la mejor he pensado en hacer una guia rapida, aqui la teneis.

Pero, antes de empezar, que es OpenSSL ?


OpenSSL es una implementacion libre (bajo una licencia apache-like) de los protocolos SSL y TLS. Implementa las funciones basicas de criptografia y provee varias funciones utiles.

Instalacion:
Esto no tiene gran misterio, instala libssl-dev desde tu repositorio de paquetes y ya esta.


Empezamos... (Ir directamente a:)


Hola mundo:

Las librerias que se van a utilizar son:
  • openssl/bio.h
  • openssl/ssl.h
  • openssl/err.h

El hola mundo seria algo asi:


#include <stdio.h>
// Cabeceras OpenSSL

#include <openssl/bio.h>
#include <openssl/ssl.h>
#include <openssl/err.h>

int main(int argc,char ** argv){

    // Iniciando OpenSSL
    SSL_load_error_strings();
    ERR_load_BIO_strings();
    OpenSSL_add_all_algorithms();

    printf("Hola, mundo de OpenSSL\n");
    return 0;
}

Hay que linkarlo con libssl (por ejemplo, compilando con el comando
gcc helloworld.c -lssl -o hello_world ).
El resultado es bastante obvio:
Hola, mundo de OpenSSL

No, no hace nada util, pero si compilo bien, se puede seguir tranquilamente... sino,(logicamente) hay un problema.

Seguimos...

Conexiones inseguras (es bastante parecido a unos sockets normales, pero sirve para famializarse con los conceptos):

El equivalente a el int <socket>; es BIO * <bio>;, esto almacenara los datos de la conexion.

La funcion para crear una nueva conexion es
<bio> = BIO_new_connect("hostname:port");
Como se puede ver, la sintaxis es bastante sencilla y no requiere montar estructuras para establecer conexiones.
Si la variable devuelta es NULL es que hubo un error creando el objeto BIO.
Para comprobar que la conexion se ha establecido se utiliza la funcion BIO_do_connect(<bio>);, si el valor devuelto es 0 o menor, no se ha podido conectar al host.

Enviar y recibir datos se hace exactamente igual que con los sockets de BSD:
-Para recibir:
BIO_read(<bio>, <buffer>, <longitud del buffer>);
(Para quien lo dude, el buffer es donde se leera la informacion, y debe ser un puntero (o un array), las otras variables son obvias ;)
El valor devuelto es el numero de bytes que se han leido, es posible que se necesite meter esta funcion en un bucle para asegurarse de que se leen todos los datos... aunque no suele haber problemas para buffer's de menos de 1Kb

-Para enviar es lo mismo:
BIO_write(<bio>, <buffer>, <longitud del buffer>);
El valor devuelto es (de nuevo) el numero de bytes enviados, sin problemas para menos de 1Kb, aun asi mejor con un bucle... ya cojeis la idea, ¿no?

Para determinar si se puede leer/escribir (enviar/recibir) en una conexion, la funcion es:
BIO_should_retry(<bio>);
Si no se puede, el valor devuelto es false ,de todas formas, en las pruebas, esta funcion causo algunos problemas (¿quiza al tratar con sockets de lectura bloqueantes?), si quieres mas informacion [ http://www.openssl.org/docs/crypto/BIO_should_retry.html ]

Un bucle simple (como este), solucionaria los posibles problemas:

int sendloop(BIO * bio,char *buf,int buflen){

    int pos=0,aux;
    while (((aux=BIO_write(bio,buf+pos,buflen-pos))<1)&&(pos>0)){

        pos+=aux;
        if (!BIO_should_retry(bio)){
            return 0;

        }
    }
    return 1;
}

Para cerrar la conexion, simplemente hacemos:
BIO_reset(<bio>);

Y para liberar la memoria:
BIO_free_all(<bio>);

Esto seria un ejemplo de cliente HTTP, con OpenSSL (se muestran las cabeceras y la pagina en si, esto se puede cambiar, pero la idea era mostrar como funcionan las conexiones):
#include <stdio.h>
#include <string.h>

// Cabeceras de OpenSSL
#include <openssl/bio.h>
#include <openssl/ssl.h>
#include <openssl/err.h>

// Bucle para enviar datos
int sendloop(BIO * bio,char *buf,int buflen){

    int pos=0,aux;
    while (((aux=BIO_write(bio,buf+pos,buflen-pos))<1)&&(pos>0)){

        pos+=aux;
        if (!BIO_should_retry(bio)){
            return 0;

        }
    }
    return 1;
}

int main(int argc,char ** argv){

    // Comprueba que los parametros son los correctos (sin interes)
    if (argc!=3){
        printf("Uso: ./conexion <hostname> <port>\n");

        exit(1);
    }
    char host_port[256];

    sprintf(host_port,"%s:%s",argv[1],argv[2]);

    BIO * bio;
    // Se crea una nueva conexion
    bio = BIO_new_connect(host_port);

    if(bio == NULL)
    {
        printf("Error en BIO_new_connect\n");

    }
    // Se comprueba que la conexion se establecio correctamente
    if(BIO_do_connect(bio) <= 0)

    {
        printf("Error al establecer conexion\n");
    }

    int i;

    char buf[512];

    // Se introduce las cabeceras HTTP en un buffer
    sprintf(buf,"GET / HTTP/1.0\r\nHOST: %s\r\n\r\n",argv[1]);

    // Y se envian
    if ((i=sendloop(bio,buf,strlen(buf)))==0){

        printf("Error al enviar cabeceras\n");
        exit(0);
    }

    char ch_buf[2];

    // Para todos los bytes que se reciben
    while ((i=BIO_read(bio,&ch_buf,1))!=0){

        if (i>0){
            // Se muestran por pantalla
            putchar(ch_buf[0]);

        }
    }

    // La conexion ha finalizado
    printf("\nConexion finalizada\n");

    // Se cierra la conexion
    BIO_reset(bio);

    // Y se libera el espacio
    BIO_free_all(bio);

}

Conexiones seguras:
Las conexiones seguras funcionan igual que las otras, la unica diferencia es en el momento de establecer la conexion...
Ademas de BIO * bio;, se utilizan los siguientes objetos:
SSL_CTX * ctx;
SSL * ssl;

Despues, hay que cargar las librerias, esto se hace con
    SSL_library_init ();
    ERR_load_BIO_strings();
    SSL_load_error_strings();
    OpenSSL_add_all_algorithms();

(Esto solo hay que hacerlo una vez en todo el programa)
El siguente paso, es crear un entorno SSL (SSL_CTX), que asignaremos a la variable ctx, esto se hace con las funciones SSL_CTX_new()  y SSLv*_method(), pasando como parametro de la primera, la salida de la segunda.
Pongo SSLv*_method, por que hay varias opciones, segun el protocolo que se utilizara, ademas cada opcion se puede utilizar para clientes (SSLv*_client_method), para servidores(SSLv*_server_method), o para los dos (SSLv*_method), para abreviar, se hablara solo de los que funcionan para ambas cosas, si prefieres una en concreto solo tienes que cambiar el nombre de la funcion(añadiendo _client o _server)...
  • SSLv2_method(): Para utilizar unicamente SSLv2 en todo el proceso
  • SSLv3_method(): Para utilizar unicamente SSLv3, esto puede producir problemas porque en las versiones que soportan varios protocolos, la conexion se suele iniciar con SSLv2
  • TLSv1_method(): Para utilizar solo TLSv1, con los mismos problemas de incompatibilidad que SSLv3_method()
  • SSLv23_method(): Para utilizar SSLv2, SSLv3 o TLS1, segun lo que soporte el otro extremos de la conexion, la conexion se iniciara como una de SSLv2 (esta es obviamente la opcion que se deberia usar a menos que haya razones para lo contrario)
Entonces, para iniciar el entorno, utilizamos:
    ctx=SSL_CTX_new(SSLv23_client_method());

Despues hay que cargar la lista de certificados fiables (al final dejo un archivo de prueba), esto se puede hacer desde un archivo o desde una carpeta, con:
SSL_CTX_load_verify_locations(<entorno>, <archivo de certificados>, <carpeta de certificados>)
Si el valor devuelto es false, es que algo fue mal.

Logicamente, no es necesario hacerlo de las dos formas, el valor que no se utilice se reemplaza por NULL, esto es lo que utilice para cargar los certificados desde un archivo:
    if(! SSL_CTX_load_verify_locations(ctx, trust_store_file, NULL))

    {
        printf("Error cargando certificados fiables\n");
        SSL_CTX_free(ctx);

        exit(0);
    }

Otra cosa, si vas a importar los certificados desde una carpeta, primero hay que prepararla para este proposito, esto se puede hacer simplemente con:
c_rehash /ruta/a/la/carpeta

El proximo paso es preparar los BIO y SSL:
    bio = BIO_new_ssl_connect(ctx);
    BIO_get_ssl(bio, & ssl);
    SSL_set_mode(ssl, SSL_MODE_AUTO_RETRY);

Una vez hecho esto, hay que establecer la conexion, como se hace con las conexiones normales:
    BIO_set_conn_hostname(bio, host_port);

De nuevo, igual que en las conexiones normales, se comprueba que la conexion fue bien:
    if(BIO_do_connect(bio) <= 0){
        printf("Error al establecer la conexion\n");
    }

Por ultimo, solo queda comprobar que el certificado es correcto, en caso de que no lo sea, queda en manos del programador cerrar la conexion o continuarla:

    if(SSL_get_verify_result(ssl) != X509_V_OK)
    {
        // No, no es valido :(
        // Pero, se puede continuar con la conexion, preguntemos al usuario
        char op;
        printf("El certificado no es valido, quieres continuar con la conexion(S/n)");
        op=getchar();
        if (op=='n'){
            SSL_CTX_free(ctx);
            exit(1);
        }
    }

El resto de la conexion se utiliza como una normal.

Al final, cuando se acabe de utilizar ese entorno ssl, hacemos:
SSL_CTX_free(ctx);

Este seria el ejemplo anterior del cliente HTTP, pero funcionando sobre SSL (recuerda que el puerto de HTTPS es 443):
#include <stdio.h>
#include <string.h>

// Cabeceras de OpenSSL
#include <openssl/bio.h>
#include <openssl/ssl.h>
#include <openssl/err.h>

// Constantes
#define trust_store_path "TrustStore/"
#define trust_store_file "TrustStore.pem"

// Bucle para enviar datos
int sendloop(BIO * bio,char *buf,int buflen){

    int pos=0,aux;
    while  (((aux=BIO_write(bio,buf+pos,buflen-pos))<1)&&(pos>0)){

        pos+=aux;
        if (!BIO_should_retry(bio)){
            return 0;

        }
    }
    return 1;
}

int main(int argc,char ** argv){

    // Comprueba que los parametros son los correctos (sin interes)
    if (argc!=3){
        printf("Uso: ./conexion <hostname> <port>\n");

        exit(1);
    }
    char host_port[256];

    sprintf(host_port,"%s:%s",argv[1],argv[2]);

    // Se preparan los objetos
    SSL_CTX * ctx;
    SSL * ssl;

    BIO * bio;

    // Y se levanta la libreria
    SSL_library_init ();

    ERR_load_BIO_strings();
    SSL_load_error_strings();
    OpenSSL_add_all_algorithms();

    ctx=SSL_CTX_new(SSLv23_method());

    // Se carga la lista de certificados fiables desde un archivo
    if(! SSL_CTX_load_verify_locations(ctx, trust_store_file, NULL))

    {
        printf("Error cargando certificados fiables\n");
        SSL_CTX_free(ctx);

        exit(0);
    }

    /*
    // Se carga la lista de certificados fiables desde una carpeta

    // Antes hay que usar este comando:
    // c_rehash /ruta/a/la/carpeta
    //
    if(! SSL_CTX_load_verify_locations(ctx, NULL, trust_store_path))
    {
        printf("Error al cargar la lista de certificados fiables\n");

        exit(1);
    }
    */

    // Configuramos el BIO y el SSL
    bio = BIO_new_ssl_connect(ctx);

    BIO_get_ssl(bio, & ssl);
    SSL_set_mode(ssl, SSL_MODE_AUTO_RETRY);

    // Se establece la conexion
    BIO_set_conn_hostname(bio, host_port);

    // Se comprueba la conexion y se realiza el "apreton de manos"

    if(BIO_do_connect(bio) <= 0){
        printf("Error al establecer la conexion\n");

        SSL_CTX_free(ctx);
        exit(1);
    }

    // Se comprueba que el certificado es valido

    if(SSL_get_verify_result(ssl) != X509_V_OK)
    {
        // No, no es valido :(

        // Pero, se puede continuar con la conexion, preguntemos al usuario
        char op;
        printf("El certificado no es valido, quieres continuar con la conexion(S/n)");
        op=getchar();

        if (op=='n'){
            SSL_CTX_free(ctx);
            exit(1);

        }
    }

    int i;
    char buf[512];
  
    // Se introduce las cabeceras HTTP en un buffer
    sprintf(buf,"GET / HTTP/1.0\r\nHOST: %s\r\n\r\n",argv[1]);

    // Y se envian
    if ((i=sendloop(bio,buf,strlen(buf)))==0){

        printf("Error al enviar cabeceras\n");
        SSL_CTX_free(ctx);
        exit(0);

    }

    char ch_buf[2];

    // Para todos los bytes que se reciben
    while ((i=BIO_read(bio,&ch_buf,1))!=0){

        if (i>0){
            // Se muestran por pantalla
            putchar(ch_buf[0]);

        }
    }

    // La conexion ha finalizado
    printf("\nConexion finalizada\n");

    // Se cierra la conexion
    BIO_reset(bio);

    // Y se libera el espacio
    BIO_free_all(bio);

    // Limpiamos los datos del SSL
    SSL_CTX_free(ctx);

}


Funciones de criptografia:
La idea no es mostrarlas todas, sino mostrar un par de ejemplos, el resto lo podeis buscar a traves de man o en http://www.openssl.org/docs/crypto/crypto.html

SHA-1 (funcion hash)

Para hacer el hash SHA-1 de un string, la libreria utilizada es:
openssl/sha.h

El uso es bastante sencillo,se utilizan 3 variables:
  • El array que se hasheara
  • La longitud del array
  • El buffer donde se guardara la salida
Ahi va el codigo:
#include <stdio.h>

#include <string.h>
#include <openssl/sha.h>

int main(int argc,char **argv){

    if (argc<2){
        printf("Uso: ./sha1 <palabra> [<palabra>] [<palabra>]\n");

    }
    int i;
    for (i=1;i<argc;i++){

        // Hasta aqui, nada interesante, viene ahora
        int digest[5];
        // Obtenemos la salida y la mostramos
        SHA1(argv[i],strlen(argv[i]),(char *)digest);

        printf("[%s] -> ",argv[i]);
        int j;

        for (j=0;j<5;j++){
            printf("%x",digest[j]);

        }
        putchar('\n');
    }
}





RC4 (cifrado simetrico):
La libreria utilizada es openssl/rc4.h

Para iniciar un cifrado, hay que crear la clave, esto se hace con RC4_set_key(<&key>,<longitud de clave>,<clave>);
Despues, solo hay que pasar los datos por RC4(<&key>,<longitud del buffer>,<buffer de entrada>,<buffer de salida>);

Por ejemplo:
#include <stdio.h>
#include <string.h>

#include <openssl/rc4.h>

#define buff_len 256

void write_all(FILE* f,void *buf,size_t n){

    int pos=0,tmp;
    while (((tmp=fwrite(buf+pos,sizeof(char),n-pos,f))>0)&&(pos<n)){

        pos+=tmp;
    }
}

int main(int argc,char **argv){

    if (argc<4){
        printf("Uso: ./rc4 <contraseña> <archivo de entrada> <archivo de salida>\n");

        exit(1);
    }

    FILE *fin,*fout;

    fin=fopen(argv[2],"r");
    if (fin==NULL){

        printf("El archivo de entrada esta vacio\n");
        exit(1);
    }

    fout=fopen(argv[3],"a");
    if (fout==NULL){

        printf("Error al crear el archivo de salida\n");
        exit(1);
    }

    RC4_KEY key;
    RC4_set_key(&key,strlen(argv[1]),argv[1]);

    int l;
    char buffer[buff_len];
    char buffer_out[buff_len];

    while ((l=fread(buffer,sizeof(char),buff_len,fin))>0){

        RC4(&key,l,buffer,buffer_out);
        write_all(fout,buffer_out,l);

    }

    fclose(fin);
    fclose(fout);
    return 0;

}

Aqui teneis en ZIP con todos los archivos: [openssl_how_to.zip]

[Referencias]
http://www.ibm.com/developerworks/views/linux/libraryview.jsp?search_by=openssl&type_by=Articles
http://www.openssl.org/docs/crypto/crypto.html

No hay comentarios:

Publicar un comentario