Cet article a pour but de présenter comment, en C, on réussit à se connecter à un serveur web qui utilise TLS. La librairie magique est openssl. La lecture de ce document vous suppose déjà au courant de ce qu'est la programmation par sockets en C, et un minimum informé.e sur ce qu'est TLS.

 Le fonctionnement général

Alors, l'idée est simple, en tous cas en théorie : on a un mécanisme de sockets déjà en place, et le but est d'utiliser openssl pour décorer la connexion et la sécuriser. Les échanges de données se font toujours par le réseau via la même socket, mais la surcouche fournie par openssl se charge des opérations de handshake, de chiffrement, et des certificats, notamment.

Le schéma général est le suivant :

  1. On créé une SSL_METHOD * qui est en substance à la fois la version de TLS qu'on utilise, et le côté (client, serveur)
  2. On créé à partir de la méthode une instance de SSL_CTX * qui va permettre la configuration générale. Par exemple, les ciphers qu'on va utiliser ou pas, les niveaux de TLS que l'on est prêt à accepter (ou pas). Ainsi, on pourra exclure le SSL3 et le TLS1.0 pour n'accepter que du TLS 1.1 ou 1.2
  3. On créé ensuite une instance de SSL * qui sera notre "décorateur" de sockets, c'est à dire qu'on a une instance de SSL * par connexion.

Passer de la méthode au contexte

Le code en lui même consiste simplement à appeler SSL_CTX_new(). A priori tout à fait surmontable. On donne un exemple plus complet, qui montre comment exclure certaines versions de TLS :

SSL_CTX * openssl_initContext(const SSL_METHOD * method) {
    SSL_CTX * context = NULL;
    // test if the method is OK
    if (method == NULL) {
        fprintf(stderr, "Version of TLS is not supported \n");
        return NULL;
    }
    // method is OK, so make the ssl context 
    context = SSL_CTX_new(method);
    if (context == NULL) {
        fprintf(stderr, "Creation of SSL_CTX failed\n");
        ERR_print_errors_fp(stderr);
        return NULL;
    }
    // ok, result is set 
    // exlclude some ciphers
    SSL_CTX_set_options(context , SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3 | SSL_OP_NO_TLSv1);
    // change mode to improve performance
    SSL_CTX_set_mode(context, SSL_MODE_AUTO_RETRY & SSL_MODE_RELEASE_BUFFERS);
    // done 
    return context;
}

Instancier un SSL *

Il n'est pas très compliqué de fabriquer une instance de SSL_CTX à partir de la méthode, mais arriver à décorer la socket est un peu plus subtil :

SSL * openssl_secureClientSocket(SSL_CTX * context, int socketFD) {
    if (context == NULL) return NULL;
    SSL * sslConnection = SSL_new(context);
    if (sslConnection == NULL) {
        fprintf(stderr, "Error creating SSL instance \n");
        return NULL;
    }
    int resultSetFD = SSL_set_fd(sslConnection, socketFD);
    if (resultSetFD <= 0) {
        fprintf(stderr, "Error when linking SSL and socket : %i\n", SSL_get_error(sslConnection, resultSetFD));
        return NULL;
    }
    int connectResult = SSL_connect(sslConnection);
    if (connectResult <= 0) {
        fprintf(stderr, "Error for ssl connection: %i\n", SSL_get_error(sslConnection, connectResult));
        return NULL;
    }
    int handshakeResult = SSL_do_handshake(sslConnection);
    if (handshakeResult <= 0) {
        fprintf(stderr, "Error during client handshake: %i\n", SSL_get_error(sslConnection, handshakeResult));
        return NULL;
    }
    return sslConnection;
}

Le code précédent est côté client : il faut un connect supplémentaire pour réussir à lier l'instance de SSL à la connexion portée par la socket. Pour la partie serveur, le code est assez similaire, il utilise accept.

Lire et écrire à partir de l'instance de SSL

Alors, toutes les opérations de lecture et d'écriture vont utiliser cette instance de SSL. Dans ce contexte, celui ci va créer un BIO, basic input output, et ce point est à la charge de la librairie, pas à la votre (sauf si vous voulez lire ce qui est effectivement transmis, en chiffré)... Les fonctions importantes sont SSL_read et SSL_write.  Notez que SSL fonctionne par états. Vous pouvez utiliser les fonctions SSL_want_read et SSL_want_write pour savoir si vous êtes bloqué.e dans un état qui n'est pas le bon. Attention : utiliser correctement SSL_read est subtil, et il sera frustrant de rester bloqué.e en attendant la fin de la transmission. A cette fin, on peut utiliser la fonction SSL_pending qui permet de savoir s'il y a des données à lire. Notre algorithme de lecture est donc le suivant :

  1. Vérifier que SSL_want_write et SSL_want_x509_lookup sont faux. En substance, soit on veut lire, soit on ne veut rien, ce qui permet de lire
  2. Lire une première fois, disons 4 ko de données. Cela fait que la méthode est bloquante.
  3. Les fois suivantes, lire en fonction de ce que nous dit SSL_pending. Attention : il faut vérifier aussi avec le protocole qu'on est bien à la fin de la lecture. Par exemple, si le serveur est lent, ou tout simplement en train de chiffrer, l'envoi est toujours en cours, mais au moment où vous appelez SSL_pending, il n'y a encore rien à lire. Faites bien attention à ce genre de cas.

Conseils pratiques

  1. Déjà, il est capital de produire des logs détaillés. openssl est une librairie subtile à comprendre, et les erreurs ne sont pas particulièrement explicites. Il convient de lire la documentation pour avoir le détail des messages d'erreur, et au moins produire un message d'erreur pour vous faciliter la mise en oeuvre
  2. Sachez que valgrind a ses limites, et qu'il est probable qu'il vous lache entre les mains quand le code sera complexe. A cette fin, il peut être utile de produire son propre compteur d'allocations et désallocations pour compenser, et mesurer à la fin de chaque test si on a libéré ce qu'on a alloué.
  3. Enfin, openssl fournit un client et un serveur de test, qui sont très utiles : si vous développez par exemple un client, vous pouvez lancer un serveur de test, et regarder ce qu'il se passe au fur et à mesure. La commande pour le lancer est, par exemple : openssl s_server -key key.pem -cert cert.pem -accept 8997 -tls1_2 -debug -HTTP (pensez à générer vos certificats avant).
  4. Si vous avez un jour envie de passer en production, ou si TLS vous intéresse, l'ANSSI propose un excellent guide pour utiliser TLS au mieux.

Bon courage !

 

Comments are closed.

Set your Twitter account name in your settings to use the TwitterBar Section.