Nei titoli e nei testi troverete qualche rimando cinematografico (ebbene si, sono un cinefilo). Se non vi interessano fate finta di non vederli, già che non sono fondamentali per la comprensione dei post...

Di questo blog ho mandato avanti, fino a Settembre 2018, anche una versione in Spagnolo. Potete trovarla su El arte de la programación en C. Buona lettura.

sabato 15 settembre 2018

Lo chiamavano Jeeg OpenSSL
come scrivere TCP Server e Client con OpenSSL in C - pt.1

Alessia: Ma che pure te c'hai a'spadalata?
Enzo/Jeeg: A spada che?
Ok, due notizie, una buona e una cattiva. La cattiva è che, esattamente come Enzo/Jeeg del gran Lo chiamavano Jeeg Robot, non c'abbiamo a'spadalata. La buona è che, per proteggere la privacy delle nostre comunicazioni ci abbiamo OpenSSL. Quindi possiamo stare (ragionevolmente) tranquilli.
...Ma che pure te c'hai er OpenSSL?...
L'argomento di oggi è molto interessante e oscuro (come quello dei due antichi post introduttivi che trovate qui e qui, ricordate?). Facciamo mente locale: l'obbiettivo è scrivere un TCP Server e un TCP Client minimali che usino OpenSSL (quindi con crittografia dei dati, chiavi, bla, bla, bla...). Se devono essere minimali dovranno, per forza, somigliare ai due esempi già presentati (o meglio, il mio obbiettivo è quello). Quindi, dopo aver letto l'articolo, vi invito a fare una semplice comparazione tra i sorgenti di riferimento e quelli nuovi (ad esempio copiandoli su file e usando un bel comparatore tipo Meld). Noterete che la somiglianza strutturale è notevole, i file sono parzialmente sovrapponibili e le differenze sono facilmente isolabili.

Ed ora bando alle ciance: cominciamo con il codice del Server:
#include "../libmyssl/myssl.h"
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <arpa/inet.h>
#include <openssl/err.h>

int main(int argc, char *argv[])
{
    // test argomenti
    if (argc != 2) {
        // errore args
        printf("%s: numero argomenti errato\n", argv[0]);
        printf("uso: %s port [i.e.: %s 8888]\n", argv[0], argv[0]);
        return EXIT_FAILURE;
    }

    // crea un socket
    int my_socket;
    if ((my_socket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0) {
        // errore socket()
        fprintf(stderr, "%s: non posso creare il socket (%s)\n", argv[0], strerror(errno));
        return EXIT_FAILURE;
    }

    // prepara la struttura sockaddr_in per questo server
    struct sockaddr_in server;              // (locale) server socket info
    memset(&server, 0, sizeof(server));
    server.sin_family = AF_INET;            // set address family
    server.sin_addr.s_addr = INADDR_ANY;    // set server address per qualunque interfaccia
    server.sin_port = htons(atoi(argv[1])); // set port number del server

    // assegnazione di un indirizzo al socket creato
    if (bind(my_socket, (struct sockaddr *)&server, sizeof(server)) < 0) {
        // errore bind()
        fprintf(stderr, "%s: errore bind (%s)", argv[0], strerror(errno));
        close(my_socket);
        return EXIT_FAILURE;
    }

    // start ascolto con una coda di max BACKLOG connessioni
    if (listen(my_socket, BACKLOG) < 0) {
        // errore listen()
        fprintf(stderr, "%s: errore listen (%s)\n", argv[0], strerror(errno));
        close(my_socket);
        return EXIT_FAILURE;
    }

    // accetta connessioni da un client entrante
    printf("%s: attesa connessioni entranti...\n", argv[0]);
    socklen_t socksize = sizeof(struct sockaddr_in);
    struct sockaddr_in client;          // (remote) client socket info
    int client_sock;
    if ((client_sock = accept(my_socket, (struct sockaddr *)&client, &socksize)) < 0) {
        // errore accept()
        fprintf(stderr, "%s: errore accept (%s)\n", argv[0], strerror(errno));
        close(my_socket);
        return EXIT_FAILURE;
    }

    // chiude il socket non più in uso
    close(my_socket);

    // preparazione per la SSL_accept(): crea un openssl context
    SSL_CTX *ctx;
    int error;
    if ((ctx = sslCreateCtx(SSL_SERVER, &error)) == NULL || error < 0) {
        // errore sslCreateCtx()
        fprintf(stderr, "%s: openssl errore creando il contesto SSL_CTX\n", argv[0]);
        ERR_print_errors_fp(stderr);
        sslClose(NULL, my_socket, ctx, false);
        return EXIT_FAILURE;
    }

    // preparazione per la SSL_accept(): crea la struttura dati corrispondente del openssl context
    SSL *ssl;
    if ((ssl = SSL_new(ctx)) == NULL) {
        // errore SSL_new()
        fprintf(stderr, "%s: openssl errore creando la struttura SSL\n", argv[0]);
        ERR_print_errors_fp(stderr);
        sslClose(ssl, client_sock, ctx, false);
        return EXIT_FAILURE;
    }

    // preparazione per la SSL_accept(): associa la struttura SSL con il socket corrente
    if (SSL_set_fd(ssl, client_sock) == 0) {
        // errore SSL_set_fd()
        fprintf(stderr, "%s: openssl errore SSL_set_fd\n", argv[0]);
        ERR_print_errors_fp(stderr);
        sslClose(ssl, client_sock, ctx, false);
        return EXIT_FAILURE;
    }

    // accetta una connessione OpenSSL con SSL_accept()
    int rc;
    if ((rc = sslFunc(SSL_accept, ssl)) != 1) {
        // errore sslAccept()
        fprintf(stderr, "%s: openssl errore accept (%d)\n", argv[0], SSL_get_error(ssl, rc));
        ERR_print_errors_fp(stderr);
        sslClose(ssl, client_sock, ctx, false);
        return EXIT_FAILURE;
    }

    // loop di ricezione messaggi dal client
    char client_msg[MYBUFSIZE];
    int recv_size;
    while ((recv_size = sslRead(ssl, client_msg, MYBUFSIZE)) > 0 ) {
        // send messaggio di ritorno al client
        printf("%s: ricevuto messaggio dal sock %d: %s\n", argv[0], client_sock, client_msg);
        char server_msg[MYBUFSIZE];
        sprintf(server_msg, "mi hai scritto: %s", client_msg);
        if (sslWrite(ssl, server_msg, strlen(server_msg)) < 0) {
            // errore sslWrite()
            fprintf(stderr, "%s: errore send (%d)\n", argv[0], SSL_get_error(ssl, recv_size));
            ERR_print_errors_fp(stderr);
            sslClose(ssl, client_sock, ctx, true);
            return EXIT_FAILURE;
        }

        // clear buffer
        memset(client_msg, 0, MYBUFSIZE);
    }

    // loop terminato: test motivo
    if (recv_size < 0) {
        // errore sslRead()
        fprintf(stderr, "%s: errore recv (%d)\n", argv[0], SSL_get_error(ssl, recv_size));
        ERR_print_errors_fp(stderr);
        sslClose(ssl, client_sock, ctx, true);
        return EXIT_FAILURE;
    }
    else if (recv_size == 0) {
        // Ok: il client si è disconnesso
        printf("%s: client disconnesso\n", argv[0]);
    }

    // esco con Ok
    sslClose(ssl, client_sock, ctx, true);
    return EXIT_SUCCESS;
}
Credo che il codice sia abbastanza chiaro e ben commentato, quindi è il caso di soffermarsi solo sulle differenze rispetto al sorgente del TCP Server di riferimento (quello del vecchio post). Le uniche varianti significative sono:
  1. una fase di accept aggiuntiva per OpenSSL dopo la fase di accept classica. Questa fase include la creazione degli oggetti OpenSSL veri e propri: un contesto (di tipo SSL_CTX*) che la libreria OpenSSL usa per gestire tutti i meccanismi di funzionamento interni, e una struttura dati (di tipo SSL*) che è un po' l'equivalente OpenSSL del descrittore di file (di tipo int) della libreria Berkeley Socket (quello creato con la chiamata a socket() un po' più sopra). Notare che con la chiamata SSL_set_fd() la liberia OpenSSL riesce ad associare il descrittore di file socket (int client_sock) con la struttura dati OpenSSL (SSL* ssl).   
  2. dal punto 1 in avanti, visto che il descrittore di file è stato sostituito da un pointer SSL, le chiamate classiche di Berkeley Socket recv(), send() e close() vengono sostituite da delle misteriose chiamate sslRead(), sslWrite() e sslClose(), che poi tanto misteriose non sono, visti i nomi.
Tutto qua per il Server, veramente poco, no? E ora possiamo passare al codice del Client. Eccolo!
#include "../libmyssl/myssl.h"
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <arpa/inet.h>
#include <openssl/err.h>

int main(int argc, char *argv[])
{
    // test argomenti
    if (argc != 3) {
        // errore args
        printf("%s: numero argomenti errato\n", argv[0]);
        printf("uso: %s host port [i.e.: %s 127.0.0.1 8888]\n", argv[0], argv[0]);
        return EXIT_FAILURE;
    }

    // crea un socket
    int my_socket;
    if ((my_socket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0) {
        // errore socket()
        fprintf(stderr, "%s: non posso creare il socket (%s)\n", argv[0], strerror(errno));
        return EXIT_FAILURE;
    }

    // prepara la struttura sockaddr_in per il server remoto
    struct sockaddr_in server;                      // (remoto) server socket info
    memset(&server, 0, sizeof(server));
    server.sin_family = AF_INET;                    // set address family
    server.sin_addr.s_addr = inet_addr(argv[1]);    // set server address
    server.sin_port = htons(atoi(argv[2]));         // set port number del server

    // connessione al server remoto
    if (connect(my_socket, (struct sockaddr *)&server, sizeof(server)) < 0) {
        // errore connect()
        fprintf(stderr, "%s: errore connect (%s)\n", argv[0], strerror(errno));
        close(my_socket);
        return EXIT_FAILURE;
    }

    // preparazione per la SSL_connect(): crea un openssl context
    SSL_CTX *ctx;
    int error;
    if ((ctx = sslCreateCtx(SSL_CLIENT, &error)) == NULL || error < 0) {
        // errore sslCreateCtx()
        fprintf(stderr, "%s: openssl errore creando il contesto SSL_CTX\n", argv[0]);
        ERR_print_errors_fp(stderr);
        sslClose(NULL, my_socket, ctx, false);
        return EXIT_FAILURE;
    }

    // preparazione per la SSL_connect(): crea la struttura dati corrispondente del openssl context
    SSL *ssl;
    if ((ssl = SSL_new(ctx)) == NULL) {
        // errore SSL_new()
        fprintf(stderr, "%s: openssl errore creando la struttura SSL\n", argv[0]);
        ERR_print_errors_fp(stderr);
        sslClose(ssl, my_socket, ctx, false);
        return EXIT_FAILURE;
    }

    // preparazione per la SSL_connect(): associa la struttura SSL con il socket corrente
    if (SSL_set_fd(ssl, my_socket) == 0) {
        // errore SSL_set_fd()
        fprintf(stderr, "%s: openssl errore SSL_set_fd\n", argv[0]);
        ERR_print_errors_fp(stderr);
        sslClose(ssl, my_socket, ctx, false);
        return EXIT_FAILURE;
    }

    // avvia una connessione OpenSSL con SSL_connect()
    int rc;
    if ((rc = sslFunc(SSL_connect, ssl)) != 1) {
        // errore sslConnect()
        fprintf(stderr, "%s: openssl errore connect (%d)\n", argv[0], SSL_get_error(ssl, rc));
        ERR_print_errors_fp(stderr);
        sslClose(ssl, my_socket, ctx, false);
        return EXIT_FAILURE;
    }

    // loop di comunicazione col server remoto
    for (;;) {
        // compone messaggio per il server remoto
        char my_msg[MYBUFSIZE];
        printf("Scrivi un messaggio per il Server remoto: ");
        scanf("%s", my_msg);

        // send messaggio al server remoto
        if ((rc = sslWrite(ssl, my_msg, strlen(my_msg))) < 0) {
            // errore sslWrite()
            fprintf(stderr, "%s: errore send (%d)\n", argv[0], SSL_get_error(ssl, rc));
            ERR_print_errors_fp(stderr);
            sslClose(ssl, my_socket, ctx, true);
            return EXIT_FAILURE;
        }

        // riceve una risposta dal server remoto
        memset(my_msg, 0, MYBUFSIZE);
        if ((rc = sslRead(ssl, my_msg, MYBUFSIZE)) < 0) {
            // errore sslRead()
            fprintf(stderr, "%s: errore recv (%d)\n", argv[0], SSL_get_error(ssl, rc));
            ERR_print_errors_fp(stderr);
            sslClose(ssl, my_socket, ctx, true);
            return EXIT_FAILURE;
        }

        // mostra la risposta
        printf("%s: risposta Server: %s\n", argv[0], my_msg);
    }

    // esco con Ok
    sslClose(ssl, my_socket, ctx, true);
    return EXIT_SUCCESS;
}
Anche il Client è, evidentemente, molto simile al riferimento, e le uniche varianti significative sono:
  1. una fase di connect aggiuntiva per OpenSSL dopo la fase di connect classica. Questa fase include la creazione degli oggetti OpenSSL veri e propri già visti qua sopra nel punto 1 del Server. Notare anche qui che con la chiamata SSL_set_fd() la liberia OpenSSL riesce ad associare il descrittore di file socket (int my_socket) con la struttura dati OpenSSL (SSL* ssl).
  2. dal punto 1 in avanti le chiamate classiche di Berkeley Socket recv(), send() e close() sono sostituite, anche in questo caso, da delle misteriose chiamate sslRead(), sslWrite() e sslClose() che poi, ripeto, tanto misteriose non sono, visti i nomi.

Tutto qua per il Client, di nuovo veramente poco, no?

Riepilogando, sembra veramente facile: creiamo contesti e strutture dati OpenSSL, aggiungiamo le fasi OpenSSL aggiuntive (accept per il Server e connect per il Client), sostituiamo le chiamate classiche con quelle misteriose et voilá! missione compiuta!

Ma no, ci deve essere un trucco, non può essere così facile... cerchiamo nel manuale di OpenSSL come funzionano le funzioni misteriose... ma non esistono! Cosa è successo? Ecco, non voglio tenervi sulle spine e passo subito a spiegare l'arcano: il trucco è evidenziato dalla linea #include "../libmyssl/myssl.h" con cui si include l'header file della libreria speciale che ho scritto per sviluppare questi Server e Client: la libreria (che ha l'originalissimo nome di libmyssl) ci fornisce dei wrapper per alcune delle funzioni tipiche di OpenSSL. E non sono dei semplici wrapper per rinominare le funzioni e offrire, magari, una lista di argomenti più facile da usare: no, sono dei wrapper abbastanza complessi, alcuni dei quali fanno molta elaborazione interna, e questo perché OpenSSL ha un funzionamento abbastanza complesso e anche l'uso non è proprio immediato.

Ma niente paura! Tanto per cominciare il primo obbiettivo l'abbiamo già raggiunto: scrivere un Server e un Client minimali che abbiano un aspetto il più possibile simile a quello dei modelli Berkeley Socket. E per forzare questo abbiamo spostato un po' di complessità in una nuova libreria (unica per Server e Client) scritta ad-hoc. Quindi che cosa ci resta da fare? Solo il secondo passo, capire come funziona la nuova libreria (e visto che l'ho scritta io penso di poter dare sufficienti dritte per raggiungere lo scopo...). Ma questo lo vedremo nella seconda parte del post...

Ciao e al prossimo post!

Nessun commento:

Posta un commento