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 24 novembre 2018

I Misteri delle Funzioni del C
come funzionano alcune funzioni speciali in C - pt.1

Mr. Neville: quattro indumenti e una scala non ci portano a un cadavere.
Sig.ra Talmann: Sig. Neville, non ho parlato di un cadavere.
Nel gran film I misteri del giardino di Compton House, un disegnatore (un programmatore?) riceve l'incarico di disegnare 12 disegni (12 funzioni?) di una aristocratica casa di campagna: si scoprirà, poi, che ogni disegno contiene e rappresenta un mistero (come funziona questa funzione?). Ebbene, girando su vari siti di programmazione (tipo l'ottimo stackoverflow, per intenderci), ho notato che, a volte, anche programmatori "scafati" hanno dubbi su concetti di base (...ammettiamolo, siamo umani...). Ecco, oggi parleremo di funzioni: sono il pane quotidiano di ogni programmatore C eppure, in alcuni casi, hanno un funzionamento misterioso. Vediamone qualcuna...
...siamo qui riuniti per spiegare una funzione misteriosa...
Cominciamo con un caso semplice semplice, che possiamo chiamare così:

1) Il mistero delle funzioni con un parametro array
Quando dobbiamo passare un array a una funzione è meglio usare come argomento un array? E quando dobbiamo passare un pointer è meglio usare un argomento pointer? Beh, visto che il caso è semplice diamo subito la risposta: È la stessa cosa! Gli argomenti array di una funzione vengono (magicamente) trasformati in pointer dal compilatore: è un comportamento standard  ben documentato anche sul mitico K&R:
"Quando il nome di un vettore viene passato ad una funzione, ciò che viene passato è la posizione dell’elemento iniziale. All’interno della funzione chiamata, questo argomento è una variabile locale, quindi un nome di vettore passato come parametro è in realtà un puntatore, ovvero una variabile che contiene un indirizzo." (Il Linguaggio C - 2a.ed. - B.W.Kernighan, D.M.Ritchie)
Comunque, già che ci siamo, vediamo due piccoli esempi che ci serviranno anche più avanti. Vai con l'esempio che usa l'array!
// prototipi locali
void myfunc(char arg[]);

// funzione main
int main(int argc, char *argv[])
{
    // set variabile e chiamo myfunc()
    char string[] = "pippo";
    myfunc(string);

    return 0;
}

// funzione myfunc con array
// anche mettendo la dimensione (i.e.: char arg[100]) il codice 
// generato è identico
void myfunc(char arg[])
{
    char a = arg[3]
}
E, a seguire, l'esempio che usa il pointer!
// prototipi locali
void myfunc(char *arg);

// funzione main
int main(int argc, char *argv[])
{
    // set variabile e chiamo myfunc()
    char *string = "pippo";
    myfunc(string);

    return 0;
}

// funzione myfunc con pointer
// questa è la versione a cui viene ricondotta dal compilatore una eventuale 
// versione con array
void myfunc(char *arg)
{
    char a = arg[3];
}
Allora: le due funzioni myFunc() sono perfettamente equivalenti, o meglio: la myFunc() con l'array si trasforma in quella con il pointer (e non importa se scriviamo anche la dimensione dell'array: 10, 100, 1000, non cambia nulla!). E per quelli che sono come San Tommaso (che non ci crede se non ci mette il naso) consiglio un piccolo esperimento: compilare per ottenere l'assembly (con gcc -S, per esempio) e verificare che il codice delle funzioni è identico, e che le uniche differenze sono nel corpo del main()... ma non dovrebbe essere praticamente identico anche quest'ultimo? Forse che "char *string = "pippo";" sviluppa codice assembly molto diverso da quello di "char string[] = "pippo";"? Ebbene si, quindi siamo pronti per analizzare un altro mistero:

2) Il mistero delle funzioni che restituiscono una stringa letterale
Sicuramente saprete tutti che una funzione non deve mai restituire l'indirizzo di una variabile locale (vabbè: una variabile automatica allocata nello stack della funzione), perché quando la funzione "esce" la variabile va out-of-scope e l'indirizzo passato al chiamante non è più valido. E allora come si fa una funzione che ritorna una stringa? Vediamo un esempio:
#include <stdio.h>

// prototipi locali
char *myfunc1(void);
char *myfunc2(void);

// funzione main
int main(int argc, char *argv[])
{
    // mostro il risultato delle funzioni
    printf("myfunc1(): %s\n", myfunc1());
    printf("myfunc2(): %s\n", myfunc2());

    return 0;
}

// funzione myfunc1 - questa funziona
char *myfunc1(void)
{
    char *string = "pippo";
    return string;  // Ok: string è (implicitamente) una variabile statica
}

// funzione myfunc2 - questa non funziona
char *myfunc2(void)
{
    char string[] = "pluto";
    return string;  // NOk: string è una variabile locale (con Warning in compilazione!)
} 
a prima vista anche myFunc1() sembrerebbe un esempio da manuale di come non fare una buona funzione! Eppure funziona (leggere i commenti), e non per caso. Il fatto è che la stringa string in myFunc1() non è una variabile locale, infatti viene (implicitamente) allocata, direttamente dal compilatore, in una zona Read Only della memoria e quindi è, a tutti gli effetti, una variabile statica. Per cui l'esempio qui sopra funziona ed è la maniera migliore di fare una funzione che ritorna una stringa. Ed attenzione: se invece usiamo un array, come è il caso della myFunc2() non funziona: questo si che viene trattato come variabile locale (ecco perché i due main() del mistero numero 1 erano diversi!). Notare, infine, che myFunc1() e la sua variabile string interna dovrebbero essere dichiarate const, visto che la stringa "pippo" ritornata non è modificabile, quindi usando il qualificatore const permettiamo al compilatore di segnalarci eventuali operazioni non permesse (provate a modificare il contenuto di una inmutable string, vedrete che bell'effetto!). Ho scritto "dovrebbero essere dichiarate const" in corsivo perché non è obbligatorio: è solo consigliabile farlo, ma non è che rende il codice a prova di bomba...

E dopo aver visto i due casi precedenti siamo ben preparati e pronti a vedere il terzo mistero:

3) Il mistero delle funzioni che restituiscono un char pointer
Nella libc ci sono un bel po' di funzioni che restituiscono un char* (per gli amici "char star"), come ad esempio quelle delle famiglia string. Come sono fatte? Ci sono alternative valide alla struttura "classica"? Vediamo un esempio con una implementazione minimale delle strcpy() e strncpy(), senza controlli, senza ottimizzazioni, senza tutto, ma per un esempio è più che sufficiente. Vediamo il codice, che include anche un piccolo e semplice main() di test:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

// prototipi locali
char *myStrcpy(char *dest, const char *src);
char *myStrncpy(char *dest, const char *src, size_t len);
char *myStrncpyMalloc(char *dest, const char *src, size_t len);

// funzione main
int main(int argc, char *argv[])
{
    // eseguo myStrcpy() e mostro il risultato
    char *src1 = "pippo";
    char dest1[32];
    printf("myStrcpy(): dest = %s\n", myStrcpy(dest1, src1));

    // eseguo myStrncpy() e mostro il risultato
    char *src2 = "paperino";
    char dest2[32];
    printf("myStrncpy(): dest = %s\n", myStrncpy(dest2, src2, strlen(src2)));

    // eseguo myStrncpyMalloc() e mostro il risultato
    char *src3 = "paperone";
    char *dest3 = myStrncpyMalloc(dest3, src3, strlen(src3));
    printf("myStrncpyMalloc(): dest = %s\n", dest3);
    free(dest3);    // ricordarsi di questa free()

    return 0;
}

// myStrcpy - versione semplificata della strcpy() della libc
char *myStrcpy(char *dest, const char *src)
{
    // salvo localmente dest
    char *my_dest = dest;

    // loop per copiare src sulla copia locale di dest
    while (*src)
       *my_dest++ = *src++;

    // ritorno il dest salvato
    return dest;
}

// myStrncpy - versione semplificata della strncpy() della libc
char *myStrncpy(char *dest, const char *src, size_t len)
{
    // salvo localmente dest
    char *my_dest = dest;

    // loop per copiare src sulla copia locale di dest (copio solo <len> caratteri)
    while (*src && len--)
       *my_dest++ = *src++;

    // aggiungo il terminatore di stringa
    *my_dest = 0;

    // ritorno il dest salvato
    return dest;
}

// myStrncpy - versione con malloc interna di myStrncpy()
char *myStrncpyMalloc(char *dest, const char *src, size_t len)
{
    // alloco dest (N.B.: la free() la deve fare il chiamante della myMemcpyMalloc())
    dest = malloc(len + 1); // il +1 è per il terminatore di stringa

    // salvo localmente dest
    char *my_dest = dest;

    // loop per copiare src sulla copia locale di dest (copio solo <len> caratteri)
    while (*src && len--)
       *my_dest++ = *src++;

    // aggiungo il terminatore di stringa
    *my_dest = 0;

    // ritorno il dest salvato
    return dest;
}
Avete notato la semplicità del meccanismo? Nella myStrcpy() Si salva localmente il destino <dest> e, in un loop, si copia "byte-a-byte" <src> sulla copia locale di <dest> (fermandosi quando si raggiunge il terminatore di stringa di <src>). Infine, si restituisce il pointer originale <dest> e non la sua copia. E perché l'originale? perchè l'operazione viene eseguita incrementando i pointer di <src> e <dest>, quindi dobbiamo restituire il pointer originale e non quello incrementato nel loop. E la myStrncpy()? Come si nota è formalmente identica alla myStrcpy(), con la unica differenza che usa un argomento addizionale <len> per copiare solo <len> caratteri (invece di arrivare fino al terminatore di stringa). Il terminatore viene aggiunto poi internamente alla funzione, proprio perché è possibile che la copia non arrivi fino alla fine della stringa originale.

E visto che volevo strafare (si, strafacciamo: Punto! Due punti!... ma sì, fai vedere che abbondiamo... Abbondandis in abbondandum...) ho creato e aggiunto anche un altra funzione, la myStrncpyMalloc(), che è una versione poco canonica della strncpy() che si occupa anche di allocare internamente (con malloc()) il destino della copia: è molto comoda, visto che possiamo passare direttamente un pointer non allocato nell'argomento <dest> (vedi il main() qui sopra), ma poi dobbiamo ricordarci di liberarlo! (vedi di nuovo il main() qua sopra). E se ci dimentichiamo di fare la free() sono dolori...

Beh, direi che con i tre misteri di oggi abbiamo già svelato molti dettagli arcani di alcune funzioni C. Ne rimangono ancora molti, e avremo tempo di parlarne più avanti nella seconda parte del post dove, sicuramente, troveremo un altro vecchio amico: void* (per gli amici "void star")... e mi raccomando: non trattenete il respiro nell'attesa...

Ciao e al prossimo post!

lunedì 8 ottobre 2018

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

Alessia: N'è pe' critica', eh! Però, amore mio, quando te trasformi te devi cambià 'ste scarpe. Cioè, 'n supereroe co' e scarpe de camoscio 'n s'è mai visto, dai! L'hai mai visto te? 'N s'è mai visto...
Ci risiamo: anche per questa seconda puntata di Lo chiamavano Jeeg Robot (oops: Lo chiamavano Jeeg OpenSSL) abbiamo due notizie, una buona e una cattiva. La buona è che per un sistema Client/Server elementare, come quello presentato nella prima parte (che avete già letto, spero...), si possono usare le funzioni OpenSSL "così come sono" (ricordate? Era un esempio "interattivo" a bassissimo volume di dati scambiati a bassissima velocità). La cattiva notizia è che nel mondo reale, quello dove un sistema Client/Server può scambiare molti dati e ad alta frequenza, le funzioni OpenSSL bisogna usarle con gli opportuni accorgimenti, se no non funziona niente. E poi mi chiedo: ma si può scrivere del buon Software con le scarpe di camoscio?
...'n openSSL co' e scarpe de camoscio 'n s'è mai visto...
Allora, come vi avevo anticipato, ho scritto una piccola libreria di smart-wrapper per alcune delle funzioni OpenSSL. La libreria si chiama libmyssl (nome originalissimo) ed è composta da due file, myssl.c e myssl.h. Vediamo subito myssl.h così ci togliamo il pensiero:
#ifndef MYSSL_H
#define MYSSL_H

#include <openssl/ssl.h>
#include <stdbool.h>

// nomi file certificati
#define RSA_SERVER_CERT     "client.pem"
#define RSA_SERVER_KEY      "key.pem"
#define RSA_CLIENT_CA_CERT  "ca.pem"

// tipi per sslCreateCtx()
#define SSL_SERVER  0
#define SSL_CLIENT  1

// timeout e iterazioni per funzioni interne openSSL: sslRead()/sslwrite()/sslFunc
#define SSL_RWTOUT  100000  // timeout per funzioni interne (e.g.: 100000 us = 100 ms)
#define SSL_RWITER  20      // numero di iterazioni in RWSSL_TOUT
                            // (e.g.: tot.timeout = 100 ms * 20 = 2 sec
// altre define
#define BACKLOG     10      // numero connessioni per coda listen(): valore ragionevole
                            // per multi-connect (e non fa danni in single-connect)
#define MYBUFSIZE   1024    // size buffer per send/recv

// prototipi globali
SSL_CTX* sslCreateCtx(int type, int *error);
int      sslWrite(SSL *ssl, const void *buf, int num);
int      sslRead(SSL *ssl, void *buf, int num);
int      sslFunc(int (*pfunc)(SSL*), SSL *ssl);
void     sslClose(SSL *ssl, int sock, SSL_CTX *ctx, bool do_shutdown);

#endif /* MYSSL_H */
Vedete? Sono quattro cose ben commentate e che rivedremo nella descrizione di myssl.c, per cui non credo sia necessario perderci in dettagli: notare solo l'uso degli include guard per il nostro header file: questa è una libreria che possiamo includere da più file dello stesso progetto, quindi bisogna premunirsi contro eventuali (erronee) inclusioni multiple. Notare poi la lista delle funzioni contenute nella libreria che non può mancare in un header file come questo: le funzioni sono quelle già viste nel codice della parte 1 del post. Ed ora bando alle ciance: passiamo all'implementazione vera e propria, vai col codice!
#include "myssl.h"
#include <unistd.h>

// prototipi locali
static bool sslRecovery(SSL *ssl, int sslresult);
static int  sslSelectRd(SSL *ssl);
static int  sslSelectWr(SSL *ssl);

////////////////////////////////////////////////////////////////////////////////
// funzioni GLOBALI
////////////////////////////////////////////////////////////////////////////////

// sslCreateCtx - crea un openSSL context
SSL_CTX* sslCreateCtx(
    int type,               // tipo di contesto: SSL_SERVER/SSL_CLIENT
    int *error)             // flag di errore
{
    // load algoritmi di encryption+hashing per SSL
    SSL_library_init();

    // load string di errore per SSL+CRYPTO APIs
    SSL_load_error_strings();

    // create una struttura SSL_METHOD (usando uno dei protocolli SSL/TLS disponibili)
    const SSL_METHOD *meth = SSLv23_method();

    // crea una struttura SSL_CTX
    *error = 0;     // senza errore per default
    SSL_CTX *my_ctx;
    if ((my_ctx = SSL_CTX_new(meth)) == NULL) {
        // errore: unset flag di errore e ritorna contesto (==NULL)
        *error = 0;
        return my_ctx;
    }

    // test modo server/client
    if (type == SSL_SERVER) {
        // SERVER: load del certificato server nella struttura SSL_CTX
        if (SSL_CTX_use_certificate_file(my_ctx, RSA_SERVER_CERT, SSL_FILETYPE_PEM) != 1) {
            // errore: set flag di errore e ritorna contesto
            *error = -1;
            return my_ctx;
        }

        // load del private-key corrispondente al certificato server
        if (SSL_CTX_use_PrivateKey_file(my_ctx, RSA_SERVER_KEY, SSL_FILETYPE_PEM) != 1) {
            // errore: set flag di errore e ritorna contesto
            *error = -1;
            return my_ctx;
        }

        // check della coerenza tra certificato server e private-key
        if (SSL_CTX_check_private_key(my_ctx) != 1) {
            // errore: set flag di errore e ritorna contesto
            *error = -1;
            return my_ctx;
        }
    }
    else {
        // CLIENT: load del certificato RSA-CA nella struttura SSL_CTX
        // (serve a verificare il certificato server sul client)
        if (SSL_CTX_load_verify_locations(my_ctx, RSA_CLIENT_CA_CERT, NULL) != 1) {
            // errore: set flag di errore e ritorna contesto
            *error = -1;
            return my_ctx;
        }

        // set flag in SSL_CTX per richiedere la verifica del certificato server
        SSL_CTX_set_verify(my_ctx, SSL_VERIFY_PEER, NULL);
        SSL_CTX_set_verify_depth(my_ctx, 1);
    }

    // unset flag di errore e ritorna un contesto valido
    *error = 0;
    return my_ctx;
}

// sslWrite - scrive dati in modo smart in una connessione SSL/TLS
int sslWrite(
    SSL        *ssl,        // struttura SSL per openSSL
    const void *buf,        // buffer dei dati da scrivere
    int        num)         // numero dei dati da scrivere
{
    int sent;

    // loop di scrittura
    int count = 0;
    for (;;) {
        // test del loop counter (tot.timeout = SSL_RWTOUT * SSL_RWITER)
        if (++count > SSL_RWITER) {
            // il timeout totale è scaduto: interrompo il loop
            // (e.g.: tot.timeout = 100 ms * 20 = 2 sec)
            break;
        }

        // scrive dati
        sent = SSL_write(ssl, buf, num);
        if (sent > 0) {
            // scrittura Ok: interrompo il loop
            break;
        }
        else {
            // scrittura NOK (risultato <= 0): start procedura di recovery
            if (sslRecovery(ssl, sent))
                continue; // ci sono ancora dati da leggere/scrivere: continuo nel loop

            // interrompo il loop
            break;
        }
    }

    // ritorna il numero di byte scritti o errore
    return sent;
}

// sslRead - legge dati in modo smart in una connessione SSL/TLS
int sslRead(
    SSL  *ssl,              // struttura SSL per openSSL
    void *buf,              // buffer dei dati da leggere
    int  num)               // numero dei dati da leggere
{
    int rcvd;

    // loop di lettura
    int count = 0;
    for (;;) {
        // test del loop counter (tot.timeout = SSL_RWTOUT * SSL_RWITER)
        if (++count > SSL_RWITER) {
            // il timeout totale è scaduto: interrompo il loop
            // (e.g.: tot.timeout = 100 ms * 20 = 2 sec)
            break;
        }

        // legge dati
        rcvd = SSL_read(ssl, buf, num);
        if (rcvd > 0) {
            // lettura Ok: interrompo il loop
            break;
        }
        else {
            // lettura NOK (risultato <= 0): start procedura di recovery
            if (sslRecovery(ssl, rcvd))
                continue; // ci sono ancora dati da leggere/scrivere: continuo nel loop

            // interrompo il loop
            break;
        }
    }

    // ritorna il numero di byte lett o errore
    return rcvd;
}

// sslFunc - wrapper smart per sslConnect()/sslAccept()/sslShutdown()
int sslFunc(
    int  (*pfunc)(SSL*),    // pointer alla funzione da eseguire
    SSL* ssl)               // struttura SSL per openSSL
{
    int result;

    // loop di esecuzione della funzione
    int count = 0;
    for (;;) {
        // test del loop counter (tot.timeout = SSL_RWTOUT * SSL_RWITER)
        if (++count > SSL_RWITER) {
            // il timeout totale è scaduto: interrompo il loop
            // (e.g.: tot.timeout = 100 ms * 20 = 2 sec)
            break;
        }

        // esegue funzione
        result = pfunc(ssl);
        if (result > 0) {
            // funzione Ok: interrompo il loop
            break;
        }
        else {
            // funzione NOK (risultato <= 0): start procedura di recovery
            if (sslRecovery(ssl, result))
                continue; // ci sono ancora dati da leggere/scrivere: continuo nel loop

            // interrompo il loop
            break;
        }
    }

    // ritorna il risultato della funzione o errore
    return result;
}

// sslClose - chiude una sessione ssl
void sslClose(
    SSL     *ssl,           // struttura SSL per openSSL
    int     sock,           // socket aperto per la sessione
    SSL_CTX *ctx,           // ssl context aperto per la sessione
    bool    do_shutdown)    // flag per eventuale esecuzione di SSL_shutdown()
{
    // libera ssl (ed esegue shutdown) se allocato
    if (ssl) {
        // eventualmente esegue shutdown
        if (do_shutdown)
            sslFunc(SSL_shutdown, ssl);

        // libera ssl
        SSL_free(ssl);
    }

    // libera socket
    close(sock);

    // libera ctx se allocato
    if (ctx)
        SSL_CTX_free(ctx);
}

////////////////////////////////////////////////////////////////////////////////
// funzioni LOCALI
////////////////////////////////////////////////////////////////////////////////

// sslRecovery - esegue una procedura di recovery su una operazione ssl fallita
static bool sslRecovery(
    SSL *ssl,               // struttura SSL per openSSL
    int sslresult)          // risultato ssl da recuperare
{
    bool result = false;    // risultato di default

    // test errore ssl
    switch (SSL_get_error(ssl, sslresult)) {
    case SSL_ERROR_WANT_READ:
        // dati non ancora disponibili: bisogna aspettare (con select())
        if (sslSelectRd(ssl) > 0)
            result = true;  // ci sono ancora dati da leggere

        break;

    case SSL_ERROR_WANT_WRITE:
        // dati non ancora scrivibili: bisogna aspettare (con select())
        if (sslSelectWr(ssl) > 0)
            result = true;  // posso ancora scrivere dati

        break;

    case SSL_ERROR_ZERO_RETURN:
        // il peer si è disconnesso: non bisogna aspettare
        break;

    default:
        break;
    }

    // ritorna il risultato del recovery
    return result;
}

// sslSelectRd - esegue una select() in lettura su un openssl file descriptor
static int sslSelectRd(
    SSL *ssl)               // struttura SSL per openSSL
{
    // ottiene il socket file descriptor associato al socket ssl
    int sock = SSL_get_rfd(ssl);

    // prepara il file descriptor set associato al socket fd <sock>
    fd_set fds;
    FD_ZERO(&fds);
    FD_SET(sock, &fds);

    // set timeout
    struct timeval timeout;
    timeout.tv_sec  = 0;
    timeout.tv_usec = SSL_RWTOUT;   // timeout

    // ritorna il risultato della select() sul file descriptor set
    return select(sock + 1, &fds, NULL, NULL, &timeout);
}

// sslSelectWr - esegue una select() in scrittura su un openssl file descriptor
static int sslSelectWr(
    SSL *ssl)               // struttura SSL per openSSL
{
    // ottiene il socket file descriptor associato al socket ssl
    int sock = SSL_get_wfd(ssl);

    // prepara il file descriptor set associato al socket fd <sock>
    fd_set fds;
    FD_ZERO(&fds);
    FD_SET(sock, &fds);

    // set timeout
    struct timeval timeout;
    timeout.tv_sec  = 0;
    timeout.tv_usec = SSL_RWTOUT;   // timeout

    // ritorna il risultato della select() sul file descriptor set
    return select(sock + 1, NULL, &fds, NULL, &timeout);
}
Ok, sono cinque funzioni, tutte ben commentate (quindi non ci sarà bisogno di descriverle linea per linea) e possiamo dividerle in due gruppi: due facili-facili che servono, praticamente, solo per aprire e chiudere l'attività: sslCreateCtx() e sslClose(), e altre tre un po' più complicate che sono gli smart-wrapper anticipati sopra: sslWrite(), sslRead() e sslFunc().

Cominciamo con sslCreateCtx(): openSSL lavora appoggiandosi su un contesto di tipo SSL_CTX* che serve come base di lavoro per tutte le operazioni della libreria. sslCreateCtx() crea questo contesto con due personalità distinte: Client e Server, quindi, come avrete notato, nel codice ci sono alcune attività comuni, e alcune attività che dipendono dal parametro type (che vale SSL_SERVER o SSL_CLIENT). Leggetevi bene i commenti per seguire il flusso che è semplice e chiarissimo. Notare solo che, visto che OpenSSL è un protocollo che usa certificati di sicurezza, sul lato Client avremo bisogno di un file-certificato di tipo CA, mentre sul lato Server avremo i due file-certificati di tipo KEY e CERT corrispondenti al CA del Client. L'argomento di questo post esula i dettagli di come funzionano e come si generano questi certificati (bisognerebbe fare un lungo post solo sull'argomento!), ma in rete c'è molta documentazione al riguardo ed è facilissimo trovare guide per poter generare senza problemi i tre file necessari per il funzionamento. sslCreateCtx() ritorna, se tutto va bene, un contesto valido (my_ctx != NULL) e senza errori (error == 0). Potrebbe però anche ritornare un contesto non valido (my_ctx == NULL) o un errore (error == -1): il chiamante deve processare tutte queste possibilità (esattamente come proposto negli esempi della parte 1 del post).

Passiamo all'altra funzione facile-facile: sslClose() serve, evidentemente, a chiudere in maniera ordinata tutto ciò che si è aperto per far funzionare il sistema Client/Server, e quindi: il contesto SSL_CTX* creato con SSL_CTX_new(), la struttura SSL* creata con SSL_new() e il socket creato con socket().

Come ben sapete "quando il gioco si fa duro i duri cominciano a giocare", quindi è venuto il momento degli smart-wrapper! Sono: sslWrite(), sslRead() e sslFunc(). I primi due servono, evidentemente, a gestire le operazioni di write e read (tramite le funzioni OpenSSL SSL_write() e SSL_read()), mentre il terzo è più generico e serve a gestire le operazioni di shutdown, accept e connect (tramite le funzioni OpenSSL SSL_shutdown(), SSL_accept() e SSL_connect()). Ovviamente non ho usato sslFunc() anche per read e write perché gli argomenti delle funzioni erano decisamente incompatibili.

Visto che i nostri smart-wrapper sono strutturalmente identici ne descriverò solo uno, ma prima di farlo dobbiamo fare mente locale e capire perché abbiamo bisogno degli smart-wrapper. Il fatto è che il funzionamento di openSSL è più complesso di quello che sembra: in una normale connessione socket quando spedisci dei dati lo fai e ti fermi li (e se sei bravo testi il codice di errore della send()). Eventuali risposte sono da gestire a un livello più alto: decido io (e non il protocollo) se un messaggio ha bisogno di una risposta. In OpenSSL non è così: fondamentalmente il problema è che ogni read implica una write implicita e viceversa. Cioè, c'è sempre una fase di negoziazione della comunicazione e, ad esempio, non si può considerare conclusa una operazione di send fatta con SSL_write() fino a quando il sistema non ci comunica che tutte le operazioni di read/write sono terminate.

Ma vediamo subito come funziona sslFunc() e tutto risulterà, magicamente, più chiaro: sslFunc() esegue la funzione che le passiamo (via function-pointer con l'argomento pfunc)). La funzione viene eseguita in loop (e qui cominciano le stranezze...), quindi viene eseguita più volte, fino a quando l'operazione è veramente terminata. Se tutto va bene (ma proprio bene!) la funzione si esegue nel loop una sola volta, e si esce con Ok. E se va male... si chiama sslRecovery() che è una funzione locale della libreria libmyssl: la sslRecovery() analizza il codice di errore che le viene passato e che può valere:
  1. SSL_ERROR_WANT_READ: dati non ancora disponibili: bisogna aspettare
  2. SSL_ERROR_WANT_WRITE: dati non ancora scrivibili: bisogna aspettare 
  3. SSL_ERROR_ZERO_RETURN: il peer si è disconnesso: non bisogna aspettare
Il caso 3 è, evidentemente, un errore irrecuperabile da comunicare al chiamante della sslRecovery(), mentre i casi 1 e 2 ci dicono solo che "bisogna aspettare" perché ci sono ancora attività di read/write da completare. E come aspettiamo? Per farlo abbiamo a disposizione una coppia di funzioni locali della libreria, e cioe sslSelectRd() e sslSelectWr()), che fanno uso della system call select() per aspettare sullo stream di read o write pertinente. La select() è una system call utile e interessantissima, su cui si potrebbe scrivere un intero post: nell'attesa (di un eventuale post) vi invito a leggere la pagina del manuale e i commenti del codice: si vede chiaramente che viene usata solo per aspettare (con il timeout che abbiamo fissato con la define SSL_RWTOUT), sperando che durante l'attesa l'operazione di I/O vada a buon fine.  

Un ultimo accenno sul loop contenuto nella sslFunc(): è infinito (uh, che rischio!) ma con il trucco: esce con un timeout nel caso qualcosa vada completamente storto (mai fidarsi dei loop infiniti...). Il timeout è ben descritto sia in myssl.h che in myssl.c, e con i valori dell'esempio dopo 2 secondi il loop termina forzatamente (con errore, ovviamente).

That's all folks!

Uff, che fatica, credo che per (almeno per il momento) l'argomento OpenSSL lo possiamo considerare chiuso. Certo che abbiamo visto un Client e un Server che invece di chiamare direttamente le funzioni OpenSSL chiamano funzioni di una libreria scritta ad-hoc, e che a loro volta chiamano funzioni locali della libreria... ma la vita del programmatore non potrebbe essere più semplice? Vabbè io in realtà così mi diverto molto... A voi sembra strano? Ma mai strano come quelli che scrivono Software con le scarpe di camoscio...

Ciao e al prossimo post!

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!