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...
...'n openSSL co' e scarpe de camoscio 'n s'è mai visto... |
#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:
- SSL_ERROR_WANT_READ: dati non ancora disponibili: bisogna aspettare
- SSL_ERROR_WANT_WRITE: dati non ancora scrivibili: bisogna aspettare
- SSL_ERROR_ZERO_RETURN: il peer si è disconnesso: non bisogna aspettare
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!
Nessun commento:
Posta un commento