Version en ligne

Tutoriel : Sécurisation d'un chat réseau Qt avec QSslSocket

Table des matières

Sécurisation d'un chat réseau Qt avec QSslSocket
SSL ? C'est qui celui-là ?
Où trouver les clés ?
Le serveur
Le client
Le test

Sécurisation d'un chat réseau Qt avec QSslSocket

SSL ? C'est qui celui-là ?

Bonjour à tous ! :)
Aujourd'hui, toutes les applications de chat connues sont cryptées. C'est pourquoi j'ai cherché pendant de nombreuses heures comment crypter le système du chat issue de ce tutoriel. Sans cryptage, n'importe quelle personne avec un minimum de compétences informatiques est capable d'espionner vos conversations les plus intimes.

Je vous mets ici le résultat de mes recherches, je tiens à préciser que je suis débutant et qu'il peut y avoir des erreurs. :-°

J'ai choisi de me servir de Qt, en utilisant QSslSocket qui implémente le protocole SSL dans un QTcpSocket. Ceci n'est pas une solution exhaustive, il a de nombreuses façons de crypter une communication en C++.

SSL ? C'est qui celui-là ?

Où trouver les clés ?

SSL est un protocole de sécurisation des échanges sur internet. Il est notamment utilisé dans les sites en « https ». Il sert à la fois à crypter les données transmises et à vérifier l’identité des acteurs du dialogue (clients et serveur).

SSL utilise le principe de la cryptographie asymétrique.
Il repose sur le principe d'une clé publique et d'une clé privée par acteur. La clé publique sert à crypter et la clé privée sert à décrypter.
La clé privée doit absolument rester PRIVÉE, c'est pour cela qu'elle est généralement stockée dans un fichier crypté par une passphrase.

A l'établissement de la connexion, les différents acteurs s'échangent leurs clés publiques, on parle de transaction :

Image utilisateur

Ainsi le serveur possède la clé publique du client1 et du client2, le client1 et le client2 possèdent celle du serveur.

Lorsque le client1 va vouloir dialoguer avec le serveur (exemple : « Salut tu vas bien ? »), il va crypter le message avec la clé publique du serveur.
Il n'y a que le serveur qui est capable de décrypter le message car il est le seul à posséder la clé privée.

Image utilisateur

L'échange serveur vers client se fera de la même façon :

Image utilisateur

Un problème de sécurité reste cependant présent :(
Vous ne le voyez pas ? :-°
Comment peut-on être sûr que l'on utilise la bonne clé publique pour crypter ? En effet un « méchant pirate » peut s'interposer entre le client et le serveur en injectant sa clé publique au client. Il sera donc capable de déchiffrer tout ce qui vient du client.

Image utilisateur

Pour résoudre ce problème de sécurité, on a crée des autorités de certification qui signent les clés publiques. Cette signature permet de certifier l'origine de la clé publique. Une clé publique signée s'appelle un certificat.

Cette page n'a pas pour but de vous former entièrement sur SSL, elle pose juste les bases nécessaires à la compréhension du reste du tutoriel. Pour plus d'informations, vous pouvez lire la bible du SSL en français.

Nous allons donc utiliser SSL pour crypter les connexions entre notre serveur de chat et les clients. Il faut donc générer des clés pour le serveur et tous les clients.

Pour obtenir plus d'information sur le cryptage, je vous invite à lire le tutoriel de L01c et Yruama.


Où trouver les clés ?

Où trouver les clés ?

SSL ? C'est qui celui-là ? Le serveur

Les clés privées et publiques ne se trouvent pas comme ça, il faut les générer !

Installation d'OpenSSL

J'ai choisi d'utiliser OpenSSL pour créer mes clés.

Installation pour Linux

Avec les sources :
Vous devez télécharger l'archive la plus récente sur le site officiel, c'est à dire ici. Ensuite il vous suffit d'effectuer les commandes suivantes

./Configure linux-elf --prefix=/usr --openssldir=/usr/openssl 
make 
su 
make install 
exit

Pour Debian :
Si vous utilisez Debian, l'installation est encore plus simple :
Si vous utilisez Debian, l'installation est encore plus simple :

apt-get install openssl

Installation pour Windows

Pour installer OpenSSL pour Windows, il vous suffit de télécharger un des installateur suivant et de réaliser l'installation.

Windows 32 bits

Windows 64 bits

A la fin de l'installation l'exécutable openSSL.exe se trouve dans " C:\OpenSSL-Win32\bin\ " ou " C:\OpenSSL-Win64\bin\ "

Génération des clés

Nous allons tout d'abord générer la clé et le certificat de notre CA (autorité de certification), j'ai choisi de faire un certificat autosigné dans le cadre du tutoriel, pour une mise en production, il faudra prévoir l'achat d'un certificat trust et la création de clé minimum 1024 bits (man openssl :p je vais pas tout faire à votre place :D).

On génère la clé privée de notre autorité

Cette clé nous permettra de signer tous les certificat émis par notre CA ! L'option des3 permet d'ajouter une passphrase pour crypter notre clé.

openssl genrsa -des3 -out ca/ca.key

Résultat :

Generating RSA private key, 512 bit long modulus
...............++++++++++++
...............++++++++++++
e is 65537 (0x10001)
Enter pass phrase for ca.key:
Verifying - Enter pass phrase for ca.key:

Notez bien cette passphrase, elle vous sera utile pour tout le reste du tuto (voir de votre vie) si vous la perdez, il vous faudra tout recommencer ! :colere2:

Une clé privée non cryptée ressemble à ça:

-----BEGIN RSA PRIVATE KEY-----
MIICXgIBAAKBgQDQG9wvnuLC4aqzaJCAWGA1AxFzg00hjPObhq1mukzsGyuuWBFG
vj/k9vFNYX55DHctb/4cXtsZRWWvgcjtYnCVwRu+DAjFsk//kOMfhplmiv9xQ+ZL
8w/Xrnm8JWdSS+S4LCMnsuIiQtLbhMrQnUV02hAtbIZiSM3k6OjShEZhDQIDAQAB
AoGAHi0cBW+1k+qjFPbBlUq7UJSMUEKmyYmlvVSPCklTZB0gfVxZzPdDTpEcNks/
yo+rLFSD9Vsvy/9LGmLoXruadWlK67PCUnpM5/oRRGgy8t73YKrxflAU5Gtymjvc
ZCf0CAs6wBft3yLU31Qc4WqVM2vTyUH76jebVhxEw8k63OUCQQD/1OmAXV+TxBPG
ZTPFbzUeAE5rQqqOW4aoMNvM61Yn/19h6SzY2MfSQvF1BNns/efCRrqOMeyvPWUG
g1okfogTAkEA0D7pDf/D2Yu5msbOAGF4QBU1erLzpi/s6Rv6VEPYCGnHQlo3jbg9
FZbjHJ4UcYyYaA8jIrkY+FIJM88YlGbWXwJBAILEdvJ5R/CFCkKf2j2yIWmLaIol
En8fw43XI5L0PB7Hxx6KDLVu4XzVYQyahTZBdqR0eMlUNZJBhJE2tO3wi2cCQQCp
JkCFd3es0BrNxqfzlThozRFofcz88za7TldydL0YcFtC4Sb4vWsYizwktZ6jcPEm
rQz8Gl9W7MO+ynwLptB/AkEA1tsnFXoYzI71enmTdugGxbv0RqAd5iQpDYQkDSdn
2LImp/3YnXNJ9qpY91j87tKthh/Oetu6SHlmLg1LOYNIdw==
-----END RSA PRIVATE KEY-----

… et une clé privée cryptée à ça:

-----BEGIN RSA PRIVATE KEY-----
Proc-Type: 4,ENCRYPTED
DEK-Info: DES-EDE3-CBC,F1AB4765D89854CA

twxvZobZKXDaJFv8LBkYlbv2OyP8/X3WeLMyJVhHauhAXzcYlxS1DBWgcp6Lb6Yt
LrYS95NSs34ymH84j9kIfu/xp3XPMy+6s/0BMAmSIr+kK1n/0KbaEJHxrEhXKr2b
UQtGa8qMC1+z9T8sDXk0CEe8Ar1C14qNseCjON4RTOXQx0HQ2LIJjPdzestZSglQ
cFk1xhibSBorsKDWIn91I9+dpDtXL3Os+FLjABkHH3JK2qsk7Tf8zEKjzuVuJJJY
mfnH0UJX8qZ3fuJmCJ4FIeh1yhUeELlxlNgxB8lcJudktBO6rD8SomppAhdfkONx
Nml97DsFTQopzKf4qDYuMPwv1EtlJu8OO+jZWHiBGn1VDvcePlTxXRdkil1/aO7D
ly9RXRUN4Y2D9G+UizBasNUiFNoHmpvudwe+XmGIwoY=
-----END RSA PRIVATE KEY-----

On peut remarquer les informations sur l'algorithme de cryptage dans les en-têtes PEM de la clé privée cryptée.(Proc-Type: 4,ENCRYPTED DEK-Info: DES-EDE3-CBC,F1AB4765D89854CA). Cela vous permet de vérifier que votre clé a bien été cryptée, sans ces en-têtes, en lisant seulement la clé je suis incapable de vous dire si elle est cryptée ou non.

On crée le certificat autosigné

Le certificat est la clé publique de notre CA signé par… heu, notre CA. Je vous rappelle que pour que votre certificat soit trust, il faut normalement le faire signer par une autre CA reconnu, ce qui vous permet de créer une CA intermédiaire. Pour avoir plus de détails sur la mise en production, je vous conseille le site de TBS

openssl req -new -x509 -key ca/ca.key -out ca/ca.pem

Je vous laisse remplir les informations demandées tout seuls, vous êtes grands ! :D
je ne vous fait pas l'affront de vous le traduire. :)

Country Name (2 letter code) [AU]:FR
State or Province Name (full name) [Some-State]:
Locality Name (eg, city) []:
Organization Name (eg, company) [Internet Widgits Pty Ltd]:
Organizational Unit Name (eg, section) []:
Common Name (eg, YOUR name) []:
Email Address []:

Le fichier généré est :

-----BEGIN CERTIFICATE-----
MIICKzCCAdWgAwIBAgIJAJ5/od5TrWDmMA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNV
BAYTAkZSMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBX
aWRnaXRzIFB0eSBMdGQwHhcNMTAwNzE1MDkxMzI5WhcNMTAwODE0MDkxMzI5WjBF
MQswCQYDVQQGEwJGUjETMBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMYSW50
ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAMzN
5M2f7NP1yohEk4tw+V3LI0wyxWe6guT7sZO0NJDE3qNqK7LE6u6INJeEFUKREdJj
DdHieoVJT/26qN0zkSMCAwEAAaOBpzCBpDAdBgNVHQ4EFgQUwLP8aXE8dKQso73D
1oXf2xYCyh0wdQYDVR0jBG4wbIAUwLP8aXE8dKQso73D1oXf2xYCyh2hSaRHMEUx
CzAJBgNVBAYTAkZSMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRl
cm5ldCBXaWRnaXRzIFB0eSBMdGSCCQCef6HeU61g5jAMBgNVHRMEBTADAQH/MA0G
CSqGSIb3DQEBBQUAA0EAAH5/Ml4mqw3rjooMStA7S1gFeDOPvSVdOCdhjiNeA2BW
j+pJH49hIKhgptZrW0GbbvjXGAYCq7y9QN676a1BHg==
-----END CERTIFICATE-----

On génère la clé privée du client

Si l'on veut crypter la clé on peut utiliser l'option -des3

openssl genrsa -out client-key.pem
Generating RSA private key, 512 bit long modulus
....................++++++++++++
...++++++++++++
e is 65537 (0x10001)

On obtient un fichier client-key.pem

On génère la demande de certificat

openssl req -new -key client-key.pem > client.csr
-----
Country Name (2 letter code) [AU]:FR
State or Province Name (full name) [Some-State]:
Locality Name (eg, city) []:
Organization Name (eg, company) [Internet Widgits Pty Ltd]:
Organizational Unit Name (eg, section) []:
Common Name (eg, YOUR name) []:
Email Address []:

Please enter the following 'extra' attributes
to be sent with your certificate request
A challenge password []:
An optional company name []:

On génére le certificat signé par notre CA

openssl x509 -req -in client.csr -out client-crt.pem -CA ca/ca.pem -CAkey ca/ca.key -CAcreateserial -CAserial ca.srl

On génère la clé du serveur

openssl genrsa -out server-key.pem

On génère la demande de certificat

openssl req -new -key server-key.pem > server.csr

On génère le certificat signé par notre CA

openssl x509 -req -in server.csr -out server-crt.pem -CA ca/ca.pem -CAkey ca/ca.key -CAcreateserial -CAserial ca.srl

Voila nous avons généré nos clés :) (le plus dur est derrière nous) !
Si vous avez plus d'un client il vous faudra générer une clé et un certificat pour chaque client.


SSL ? C'est qui celui-là ? Le serveur

Le serveur

Où trouver les clés ? Le client

Pour suivre cette partie, vous devez connaitre Qt. Si ce n'est pas le cas il faut commencer par !

Qt, depuis sa version 4.3, permet d'utiliser des socket SSL via la classe QSslSocket, je vous invite à ouvrir la documentation en parallèle de ce tutoriel car c'est cette classe que nous allons utiliser ici.

J'ai choisi de ne pas vous mettre le code complet du serveur, juste les parties qui utilisent SSL, :( ne soyez pas tristes vous trouverez tous les éléments manquants ici.

J'ai choisi d'utiliser 3 classes :

La classe Serveur

#ifndef SERVEUR_H
#define SERVEUR_H

#include <QTcpServer>
#include <ClientServeur.h>

class Serveur : public QTcpServer
{
        Q_OBJECT

public:
        Serveur( QObject *parent = 0 );

signals:
        void nouveauClient(ClientServeur *client);
        void sslErreur(const QString &error);

protected:
        void incomingConnection( int descriptionSocket );

};

#endif
#include "Serveur.h"

#include "ClientServeur.h"

Serveur::Serveur( QObject *parent ) :QTcpServer( parent )
{
}

//on surcharge la methode incomingConnection
//le but ici est de récupérer un QSslSocket au lieu d'un QTcpSocket
void Serveur::incomingConnection( int descriptionSocket )
{
    ClientServeur *client    =new ClientServeur( descriptionSocket, this );
    connect(client, SIGNAL(sslError(const QString&)),this,  SIGNAL(sslErreur(const QString&))  );
    emit nouveauClient(client);
}

Cette classe est obligatoire, elle hérite de QTcpServer et surchage la méthode incomingConnection.
Dans cette méthode j'instancie un ClientServeur qui hérite de QSslSocket.

La classe ClientServeur

#ifndef CLIENTSERVEUR_H
#define CLIENTSERVEUR_H

#include <QSslSocket>
#include <QtGui>
#include <QtNetwork>
class ClientServeur: public QSslSocket
{
        Q_OBJECT

public:
        ClientServeur( int descriptionSocket, QObject *parent );

signals:
        void sslError(const QString &erreur);

private slots:
        void sslErreur( const QList<QSslError> &err );

};

#endif
#include "ClientServeur.h"

ClientServeur::ClientServeur( int descriptionSocket, QObject *parent )
        : QSslSocket( parent )
{
        connect( this, SIGNAL(disconnected()), SLOT(deleteLater()) );
        connect( this, SIGNAL(sslErrors(QList<QSslError>)),
                        SLOT(sslErrors(QList<QSslError>)) );

        if( !setSocketDescriptor( descriptionSocket ) )
        {
                return;
        }

        //on charge la clé privé    
        QFile file( "../../key/server-key.pem" );
        if( ! file.open( QIODevice::ReadOnly ) )
        {
            qDebug() << "ouverture de la clé impossible" << "../../key/server-key.pem";
            return;
        }
        QSslKey key( &file, QSsl::Rsa, QSsl::Pem, QSsl::PrivateKey, "mapassphrase" );
        file.close();
        setPrivateKey( key );
        //on charge le certificat de la ca
        if( ! addCaCertificates( "../../key/ca/ca.pem" ) )
        {
            qDebug() << "ouverture du certificat de la ca impossible" << "../../key/ca/ca.pem";
            return;
        }
        
        //on charge le certificat du serveur
        setLocalCertificate( "../../key/server-crt.pem" );
        
        //j'enleve la vérification des clients, mon but ici est de crypter la connexion pas de  vérifier l'identité des différents clients 
        setPeerVerifyMode(QSslSocket::VerifyNone);
        
        //on ignore les erreurs ssl car on a un certificat autosigné qui est donc non sure, on a la possiblité de choisir les erreurs à ignorer en les passant en arguments à la méthode
        ignoreSslErrors();
        
        //on demarre le cryptage
        startServerEncryption();
}

void ClientServeur::sslErreur( const QList<QSslError> &errors )
{
        foreach( const QSslError &error, errors )
        {
            emit sslError(QString::number(error.error()));
        }
}

Cette classe va charger la clé et le certificat de notre serveur, elle va aussi charger le certificat de notre CA. Ce certificat est utilisé si on active la vérification des peers dans SSL avec l'option setPeerVerifyMode. J'ai choisi ici de supprimer cette vérification pour rendre le tutoriel plus simple.
N'oubliez pas de changer « mapassphrase » par votre passphrase (enregistrée lors de la génération des clés).

La classe FenServeur

Dans la classe FenServeur, il suffit juste d'instancier un Server et de connecter les signaux.

serveur = new Serveur(this);
if (!serveur->listen(QHostAddress::Any, 50885)) // Démarrage du serveur sur toutes les IP disponibles et sur le port 50585
{
    // Si le serveur n'a pas été démarré correctement
    etatServeur->setText(tr("Le serveur n'a pas pu être démarré. Raison :<br />") + serveur->errorString());
}
else
{
    // Si le serveur a été démarré correctement
    etatServeur->setText(tr("Le serveur a été démarré sur le port1 <strong>") + QString::number(serveur->serverPort()) + tr("</strong>.<br />Des clients peuvent maintenant se connecter."));
    connect(serveur, SIGNAL(nouveauClient(ClientServeur*)), this, SLOT(nouvelleConnexion(ClientServeur*)));
    connect(serveur, SIGNAL(sslErrors(const QString &)),this,  SLOT(sslErreur(const QString &))  );
}
//on affiche les erreurs ssl
void FenServeur::sslErreur(const QString &erreur)
{
    QMessageBox::warning(this,"erreur",erreur);
}
//on enregistre les nouveau client
void FenServeur::nouvelleConnexion(ClientServer* nouveauClient)
{
        etatServeur->setText("client "+QString(nouveauClient->socketDescriptor()));
        connect(nouveauClient, SIGNAL(readyRead()), this, SLOT(donneesRecues()));
        connect(nouveauClient, SIGNAL(disconnected()), this, SLOT(deconnexionClient()));
}
//le serveur recoit des données
void FenServeur::donneesRecues()
{
        MyClient *client=NULL;
        // On détermine quel client envoie le message (recherche du QSslSocket du client)
        //on cast le sender() pour obtenir  le QSslSocket du client
       QSslSocket *socket = qobject_cast<QSslSocket *>(sender());

        if (socket == 0) // Si par hasard on n'a pas trouvé le client à l'origine du signal, on arrête la méthode
       {
            qDebug() << "pas de client à l'origine du signal";
            return;
        }

..... Traitement des données recues

}

Dans cette classe on connecte les signaux qui permettront de traiter les erreurs et la réception des données des clients.

Petite astuce :

Il est possible de récupérer les informations saisies lors de la création du certificat par exemple pour récupérer le CN :

socket->peerCertificate().subjectInfo(QSslCertificate::CommonName)

Voilà, notre serveur est terminé, si vous avez des problèmes de compilation vérifiez la présence de network dans votre fichier.pro (QT += network).


Où trouver les clés ? Le client

Le client

Le serveur Le test

J'ai choisi de ne pas vous mettre le code complet du client, juste les parties qui utilisent SSL, :( ne soyez pas triste vous trouverez tous les éléments manquants ici.

//on instancie un QSslSocket
socket = new  QSslSocket(this);


//on connect les différents signaux
    connect(socket, SIGNAL(encrypted()), this, SLOT(ready()));
    connect(socket, SIGNAL(readyRead()), this, SLOT(donneesRecues()));
    connect(socket, SIGNAL(connected()), this, SLOT(connecte()));
    connect(socket, SIGNAL(disconnected()), this, SLOT(deconnecte()));

//on charge la clé privé du client
QFile file( "../../key/client-key.pem" );
if( ! file.open( QIODevice::ReadOnly ) )
{
    qDebug() << "la clé du client ne peut pas être chargé" <<"../../key/client-key.pem";
    return;
}
QSslKey key( &file, QSsl::Rsa, QSsl::Pem, QSsl::PrivateKey, "mapassphrase" );
file.close();
socket->setPrivateKey( key );
    
//on charge le certificat du client
socket-> setLocalCertificate( "../../key/client-crt.pem" );
    
//on charge le certificat de notre ca
if( ! addCaCertificates( "../../key/ca/ca.pem" ) )
{
     qDebug() << "ouverture du certificat de la ca impossible" << "../../key/ca/ca.pem";
     return;
}
//on supprime la vérification du serveur
socket->setPeerVerifyMode(QSslSocket::VerifyNone);
    
//on ignore les erreurs car on a un certificat auto signé
socket->ignoreSslErrors();
    
// On désactive les connexions précédentes s'il y en a
socket->abort(); 
    
//on se connecte au serveur
socket->connectToHostEncrypted(serveurIP->text(), serveurPort->value());

//on commence l'encryptage
socket->startClientEncryption();

Il faut attendre l'émission du signal encrypted pour pouvoir envoyer les données cryptées.

Cette classe instancie un QSslSocket et connecte les différents signaux aux slots de traitement. Pour que SSL fonctionne, il faut charger la clé, le certificat client et le certificat de notre CA. Une fois que tous les éléments sont chargés, on se connecte à notre serveur via connectToHostEncrypted.
N'oubliez pas de changer « mapassphrase » par votre passphrase (enregistrée lors de la génération des clés).


Le serveur Le test

Le test

Le client

Pourquoi et comment tester ?

Vu que je ne fais confiance qu'à ce que je vois :D et que la conversation entre mon client et mon serveur est super confidentielle, je préfère être sûr que mon cryptage fonctionne ;).

Pour débugger et vérifier que ce que j'ai programmé fonctionne, j'utilise souvent Wireshark. Il permet de capturer tous les paquets qui passent par ma carte réseau. Ainsi cela me permet d'avoir une vision réseau de mon application en fonctionnement. Le but ici est de vérifier que la communication entre mon client et mon serveur est bien cryptée.

Voici une vidéo qui vous explique comment utiliser Wireshark, malheureusement pour certains elle est en anglais.

http://www.dailymotion.com/swf/video/xc7c7u_introduction-a-wireshark_tech

Le résultat du test

Image utilisateur

Les deux premières trames initialisent la connexion TCP, elles sont donc non cryptées. Une fois la connexion établie, on remarque que toutes les connexions sont cryptées ! ^^
On a donc atteint notre but et plus personne ne peut écouter nos conversations passionnantes sur le chat !

J'espère que cela aidera quelques personnes ;)
Vous pouvez utiliser SSL avec tous les programmes qui ont besoin d'envoyer des informations sur le réseau, par exemple l'envoi de photos en mode crypté, un système de login-mot de passe sur un serveur, et vous pouvez même utiliser les certificats pour faire un login automatique d'un utilisateur.

Je suis ouvert à toute suggestion et amélioration, c'est comme cela que l'on devient meilleur :D


Le client