Passer à SSL

a marqué ce sujet comme résolu.

Bonjour à tous,

Je développe un jeu. Le serveur est en Java et le client en C++. J’aimerais passer à SSL, mais je n’arrive pas à le faire correctement jusqu’au bout.

En suivant quelques ressources difficilement trouvées sur le net, j’ai réussi à faire en sorte que ça marche… presque.

La connexion sur le serveur est bien établie, la communication est bien chiffrée, mais ça pêche du côté de la vérification du certificat. Pour le moment, sur le client, je désactive la vérification du certificat provenant du serveur. Si je l’active ça ne fonctionne pas, le client refuse de se connecter. L’objectif c’est bien sûr que le client accepte le certificat du serveur… car sans cela, je perds évidemment une bonne partie de la sécurité.

Code d’initialisation d’OpenSSL:

1
2
3
4
5
6
7
SSL_library_init();
OpenSSL_add_all_algorithms();
SSL_load_error_strings();
SSL_METHOD* method = TLSv1_2_client_method();
ctx = SSL_CTX_new(method);
//SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER, 0); //C'est cette ligne qui permet d'activer la vérification du certificat provenant du serveur
SSL_CTX_load_verify_locations(ctx, (APPDIR + "cert.pem").c_str(), NULL);

Une partie du code de connexion au serveur:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
ssl = SSL_new(ctx);
SSL_set_fd(ssl, sock); // sock c'est un handle sur le socket TCP, déjà connecté à ce moment-là
int sslre = SSL_connect(ssl);
int sslvr = SSL_get_verify_result(ssl);
X509* cert = SSL_get_peer_certificate(ssl);
if (!cert) debug << "No certificate returned by the server !" << endl;
else X509_free(cert);
debug << "SSL_connect returned " << sslre << endl;
debug << "Last error = " << GetLastError() << endl;
debug << "SSL_get_error = " << SSL_get_error(ssl, sslre) << endl;
debug << "SSL_get_verify_result = " << sslvr  << endl; 
debug << "SSL_state_string = " << SSL_state_string(ssl) << endl; 
debug << "SSL_state_string_long = " << SSL_state_string_long(ssl) << endl; 

Si j’active la vérification, le message "No certificate returned by the server !" s’affiche, et je reçois systématiquement une erreur 20 de la fonction SSL_get_verify_result. La doc que j’ai bien eu du mal à trouver m’en dit ceci:

20 X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT_LOCALLY: unable to get local issuer certificate the issuer certificate could not be found: this occurs if the issuer certificate of an untrusted certificate cannot be found.

D’après ce que j’en comprends, il ne retrouve pas un certificat auquel il peut faire confiance dans ce que lui envoie le serveur. J’ai un certificat Letsencrypt installé sur le serveur.

A partir de là je ne comprends pas trop ce que je dois faire pour que ça marche…

Première question: Est-ce que le certificat renvoyé par le serveur est vraiment correct ? Comment est-ce que je peux le vérifier ?

Deuxième question: puis-je faire en sorte qu’il n’y ait pas besoin de mettre à jour le client tous les trois mois à cause du certificat (durée de validité d’un certificat Letsencrypt) ? Ca serait vraiment une tarre, même avec une mise à jour automatique il y a pas mal de joueurs qui ne savent pas comment faire, qui ont du mal de le faire, ou qui ont peur. En plus très peu de monde savent ce qu’est SSL et ce que ça apporte… en bref les joueurs ne sont pas nécessairement versés dans la technique (j’ai des joueurs qui ont passé 70 ans, si si je vous assure). Je tiens quand même à tout passer sur SSL, c’est fondamentalement une bonne chose, même si à la base c’est parce que 1/Google met la pression, et 2/un bon groupe de joueurs l’ont aussi demandé.

Troisième question: Je ne comprends pas trop ce que je dois lui donner comme fichier dans SSL_CTX_load_verify_locations. J’ai essayé plusieurs choses qui soit ne changent rien (toujours l’erreur 20), ou soit renvoie l’erreur 2 au lieu de 20 dans SSL_get_verify_result:

L’erreur 2 dit:

2 X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT: unable to get issuer certificate the issuer certificate of a looked up certificate could not be found. This normally means the list of trusted certificates is not complete.

Visiblement les fichiers qui donnent cette erreur 2 ne sont pas les bons, encore moins. Voici tous ceux que j’ai essayés jusqu’à présent sans succès:

  • Le fichier cert.pem généré lors de l’enregistrement chez Letsencrypt, présent sur le serveur dans /etc/letsencrypt/live/domaine.tld/
  • chain.pem aussi généré au même endroit
  • fullchain.pem aussi généré à la même place
  • un fichier all.pem qui est la concaténation des trois précédents (cat cert.pem chain.pem fullchain.pem > all.pem)
  • ISRG Root X1 de https://letsencrypt.org/certificates/
  • X3 cross signed du même site
  • Authority X3 du même site

Je n’ai pas essayé privkey.pem mais c’est celui qui contient la clé privée, donc à ne surtout pas distribuer non ?

Bien entendu, je suis certain que le fichier que je lui donne dans SSL_CTX_load_verify_locations est bien lu (la fonction renvoie 1 en cas de succès, si le fichier n’existe pas elle renvoie 0).

Bien évidemment je ne suis pas fou, je n’ai pas compilé OpenSSL moi-même. J’ai voulu essayer au début, mais j’ai vite abandonné, je ne comprends rien. Heureusement on trouve des binaires assez facilement, par exemple ici (je ne me souviens plus si c’était là que je la’vais pris) J’ai la version 1.1.0c, ce n’est pas la toute dernière, ce n’est pas bien au niveau de la sécurité, mais pour le code ça ne change sûrement pas grand chose.

Comme vous aurez pu le constater, le serveur tourne sous linux, et le client est exclusivement prévu pour windows (pas de version multiplateforme prévue).

Du côté du serveur qui est en Java, je ne pense pas être faux. J’ai trouvé un script qui permet de convertir les .pem de Letsencrypt et la clé privée dans le format keystore accepté par Java. Ensuite, voici un bout de code permettant de créer le socket d’écoute, simplifié au maximum:

1
2
3
4
5
System.setProperty("javax.net.ssl.keyStore", Config.get("keystore"));
System.setProperty("javax.net.ssl.keyStorePassword", Config.get("keystorePassword"));
server = SSLServerSocketFactory .getDefault() .createServerSocket();
server.bind(new InetSocketAddress(host, port), backlog);
while(true) newConnection(server.accept());

Aucun moyen de savoir si le fichier keystore est bien pris en compte, mais comme si je n’en mets pas on ne peut pas se connecter même sans la vérification côté client, j’en conclus que ça doit être bon.

Merci pour l’aide que vous pourrez apporter !

+0 -0

Salut,

Ton lien ne fonctionne pas, il me rouvre la page du sujet.

Les trucs de BSD ne vont probablement pas m’aider vu que le client C++ est exclusivement sous windows et le restera, à moins que je n’aie pas compris ce que tu rpoposes ?

+0 -0

Ton lien ne fonctionne pas, il me rouvre la page du sujet.

QuentinC

Arf ! Désolé, c’est fixé.

Les trucs de BSD ne vont probablement pas m’aider vu que le client C++ est exclusivement sous windows et le restera, à moins que je n’aie pas compris ce que tu rpoposes ?

QuentinC

Au temps pour moi, je n’avais pas lu que on client tournait sous Windows. Du coup, effectivement, il y a peu de chance que la bibliothèque ait été portée ou puisse l’être vers Windows (du moins facilement). Cela étant, je te donnais également le lien car il y a plusieurs comparaisons et explications de code, ce qui pourra peut-être t’aider.

+0 -0

Merci pour le lien. Je ne savais pas qu’OpenSSL avait été forké.

En fait il semblerait que chez LibreSSL ils soient beaucoup moins cons, j’ai trouvé un .zip qui contient les DLL, les .lib et les .h pour windows, en à peine quelques minutes sur leur site, et en plus totalement à jour:https://ftp.openbsd.org/pub/OpenBSD/LibreSSL/libressl-2.5.2-windows.zip

Là où j’ai bien galéré pour trouver un binaire déjà compilé d’OpenSSL, on va dire pas trop démodé. La 1.1.0c que j’avais trouvée date de novembre 2016, mais j’ai bien failli ne pas trouver de version plus récente que 2011. Quand même, je trouve que ça craint, de ne pas trouver un binaire précompilé à jour, pour un projet aussi important qu’OpenSSL.

Du coup je vais voir ce que je peux faire de LibreSSL. Même si ça ne résoudra peut-être pas mon problème de fond, ça a l’air quand même beaucoup mieux documenté.

Merci.

+0 -0

Je reviens vers vous. J’ai opté pour LibreSSL qui est effectivement beaucoup mieux documenté. Par contre mon problème de fond est toujours présent, je n’arrive toujours pas à me connecter si je ne désactive pas la vérification du certificat.

Au moins les erreurs de LibreSSL sont beaucoup plus simples à trouver et bien mieux décrites. En plus je ne l’ai pas remarqué tout de suite mais le build d’OpenSSL que j’avais récupéré était linké à une autre version de la MSVCRT que celle que j’utilise ailleurs dans mon code; ça aurait été un nid à problèmes potentiels plus tard.

Donc voici ce que j’arrive à obtenir quand j’essaie de me connecter:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
before handshake TLS error = NULL
after handshake TLS error = certificate verification failed: unable to get issuer certificate
tls_peer_cert_provided = 0
tls_conn_version = NULL
tls_conn_cipher = NULL
tls_peer_cert_subject = NULL
tls_peer_cert_issuer = NULL
tls_peer_ocsp_cert_status = -1
tls_peer_ocsp_crl_reason = -1
tls_peer_ocsp_response_status = -1
tls_peer_ocsp_result = NULL
tls_peer_ocsp_url = NULL

Suite à quelques conseils trouvés ailleurs, j’ai également tenté de me connecter en utilisant le s_client de l’utilitaire openssl. Voici ce que j’obtiens:

J’ai volontairement remplacé le vrai nom de domaine de mon serveur par "mon.serveur.net"; mais dans la sortie réelle on voit bien mon nom de domaine, sous-domaine inclus; car je me connecte en fait à un sous-domaine, pas au domaine principal ("mon.serveur.net" et pas simplement "serveur.net")

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
> openssl s_client -connect mon.serveur.net:1443
CONNECTED(0000017C)
---
Certificate chain
 0 s:/CN=mon.serveur.net
   i:/C=US/O=Let's Encrypt/CN=Let's Encrypt Authority X3
 1 s:/C=US/O=Let's Encrypt/CN=Let's Encrypt Authority X3
   i:/O=Digital Signature Trust Co./CN=DST Root CA X3
---
Server certificate
-----BEGIN CERTIFICATE-----
... certificat encodé en base64 ...
-----END CERTIFICATE-----
subject=/CN=mon.serveur.net
issuer=/C=US/O=Let's Encrypt/CN=Let's Encrypt Authority X3
---
No client certificate CA names sent
Server Temp Key: ECDH, P-256, 256 bits
---
SSL handshake has read 2949 bytes and written 366 bytes
---
New, TLSv1/SSLv3, Cipher is ECDHE-RSA-AES128-GCM-SHA256
Server public key is 2048 bit
Secure Renegotiation IS supported
Compression: NONE
Expansion: NONE
No ALPN negotiated
SSL-Session:
    Protocol  : TLSv1.2
    Cipher    : ECDHE-RSA-AES128-GCM-SHA256
    Session-ID: 58E7......
    Session-ID-ctx: 
    Master-Key: FE78......
    Start Time: 1491...
    Timeout   : 7200 (sec)
    Verify return code: 20 (unable to get local issuer certificate)
---
depth=1 C = US, O = Let's Encrypt, CN = Let's Encrypt Authority X3
verify error:num=20:unable to get local issuer certificate
verify return:0
write:errno=32

Comment je fais pour déboguer maintenant ? Merci.

+0 -0

A priori, ce genre d’erreur survient lorsque la chaîne de certificats ne peut pas être vérifiée. Tu es bien certains que les certificats composant cette chaîne (à l’exception de celui de ton serveur) sont bien sur ta machine client ?

+0 -0

Tu es bien certains que les certificats composant cette chaîne (à l’exception de celui de ton serveur) sont bien sur ta machine client ?

En fait je n’en sais absolument rien du tout. Je dois probablement mal m’y prendre.

Voici mon code d’initialisation:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
bool SSLSocket::initialize (void) {
if (
!Socket::initialize()
|| tls_init()
|| !(ctx = tls_config_new())
|| tls_config_set_ca_file(ctx, (APPDIR + "cert.pem").c_str() )
) {
Beep(1600, 100);
debug << "Config error ! " << tls_config_error(ctx)) << endl;
}
return !!ctx;
}

JE n’ai ni le bip ni le message, ça signifie donc qu’il a correctement chargé le fichier .pem que je lui ai donné. J’en conclus donc que ce que je lui donne ne le satisfait pas… Voici le début et la fin de ce fichier:

1
2
3
-----BEGIN CERTIFICATE-----
MIIFjTCCA3WgAwIB...BK2yAinWEsikxqEt
-----END CERTIFICATE-----

Ce qui correspond au certificat racine de Letsencrypt x3:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
> openssl x509 -text -in cert.pem
Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number:
            d3:b1:...
    Signature Algorithm: sha256WithRSAEncryption
        Issuer: C=US, O=Internet Security Research Group, CN=ISRG Root X1
        Validity
            Not Before: Oct  6 15:43:55 2016 GMT
            Not After : Oct  6 15:43:55 2021 GMT
        Subject: C=US, O=Let's Encrypt, CN=Let's Encrypt Authority X3
        Subject Public Key Info:
            Public Key Algorithm: rsaEncryption
                Public-Key: (2048 bit)
                Modulus:
                    00:9c:d3:0c:...
                Exponent: 65537 (0x10001)
        X509v3 extensions:
            X509v3 Key Usage: critical
                Digital Signature, Certificate Sign, CRL Sign
            X509v3 Basic Constraints: critical
                CA:TRUE, pathlen:0
...

J’ai coupé la fin de la sortie qui est assez longue et je n’ai pas l’impression que la suite est très utile.

J’en ai aussi essayé un autre, qui ne passe pas non plus:

1
2
3
-----BEGIN CERTIFICATE-----
MIIEkjCCA.../DNFu0Qg==
-----END CERTIFICATE-----
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
> openssl x509 -text -in cert.pem
Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number:
            0a:01:41:42:...
    Signature Algorithm: sha256WithRSAEncryption
        Issuer: O=Digital Signature Trust Co., CN=DST Root CA X3
        Validity
            Not Before: Mar 17 16:40:46 2016 GMT
            Not After : Mar 17 16:40:46 2021 GMT
        Subject: C=US, O=Let's Encrypt, CN=Let's Encrypt Authority X3
        Subject Public Key Info:
            Public Key Algorithm: rsaEncryption
                Public-Key: (2048 bit)
                Modulus:
                    00:9c:d3:0c:f0:5...
                Exponent: 65537 (0x10001)
        X509v3 extensions:
            X509v3 Basic Constraints: critical
                CA:TRUE, pathlen:0
...

J’ai téléchargé le premier sur le site de letsencrypt (voir URL dans mon premier post), le second fait partie de la chaîne retournée par mon serveur à la connexion, en deuxième position juste derrière mon certificat signé par Letsencrypt.

La ligne CA:TRUE, pathlen:0 me laisse supposer qu’ils sont pourtant bien reconnus comme certificats racine. Pour lever tout doute j’ai bien utilisé l’implémentation de LibreSSL de l’utilitaire openssl dans les commandes ci-dessus.

J’ai lancé un test de mon serveur sur https://www.sslshopper.com/ssl-checker.html et j’obtiens en gros: "The certificate should be trusted by all major web browsers (all the correct intermediate certificates are installed)."

Mon problème est sûrement dans mon code, pas sur le serveur… mais je ne vois vraiment pas où.

Merci.

+0 -0
1
bool SSLSocket::initialize (void) {

Raaaaaah ! Cachez ce C++ que je ne saurais voir ! :pirate:

Hum… Plus sérieusement, je me demande si le soucis ne viens pas de l’appel à la fonction tls_config_set_ca_file(). Celle-ci te permet a priori de spécifier un fichier contenant tous les certificats racines (et les autres). Du coup, si ton fichier cert.pem correspond uniquement au certificat racine de Let’s Encrypt, c’est normal que ça râle derrière. Si c’est bien cela, tu dois juste t’assurer que ce certificat racine (et les intermédiaires, me semble-t-il) se situent dans le répertoire ou le fichier utilisé par ton système pour stocker les certificats racines (je ne sais pas où Windows place ces derniers).

Si ce n’est pas cela, essaye d’abord un code simple sans classe (hu hu) en décomposant bien chaque appel afin de détecter où tu rencontres ton problème. Déjà, est-ce que le code suivant fonctionne de ton côté ?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
#include <stdio.h>
#include <stdlib.h>
#include <tls.h>


static char buf[255];
static char http_get[] = \
    "GET /index.html HTTP/1.1\r\n" \
    "Host: www.zestedesavoir.com\r\n" \
    "\r\n";


int
main(void)
{
    struct tls *ctx;

    if (tls_init() != 0) {
        perror("tls_init");
        return EXIT_FAILURE;
    }

    ctx = tls_client();

    if (ctx == NULL) {
        perror("tls_client");
        return EXIT_FAILURE;
    }

    if (tls_connect(ctx, "www.zestedesavoir.com", "443") != 0) {
        fprintf(stderr, "tls_connect: %s\n", tls_error(ctx));
        return EXIT_FAILURE;
    }

    if (tls_handshake(ctx) != 0) {
        fprintf(stderr, "tls_handshake: %s\n", tls_error(ctx));
        return EXIT_FAILURE;
    }

    if (tls_write(ctx, http_get, sizeof http_get - 1) == -1) {
        fprintf(stderr, "tls_write: %s\n", tls_error(ctx));
        return EXIT_FAILURE;
    }

    if (tls_read(ctx, buf, sizeof buf) == -1) {
        fprintf(stderr, "tls_read: %s\n", tls_error(ctx));
        return EXIT_FAILURE;
    }

    puts(buf);
    tls_close(ctx);
    tls_free(ctx);
    return 0;
}
+0 -0

Raaaaaah ! Cachez ce C++ que je ne saurais voir !

Heu ? Je ne vois pas ce qu’il y a de mal ? Ou bien c’est juste que tu na’imes pas le C++ ?

Si ce n’est pas cela, essaye d’abord un code simple sans classe (hu hu) en décomposant bien chaque appel afin de détecter où tu rencontres ton problème. Déjà, est-ce que le code suivant fonctionne de ton côté ?

Non, ça crash, mais j’ai finalement compris pourquoi en cherchant comment créer une banque de certificats (trust store) avec l’utilitaire openssl.

Il fallait juste concaténer les deux certificats et les mettre dans le même fichier .pem pour que ça marche. En premier celui de Letsencrypt x3 signé par ISRG x1, puis ensuite ISRG x1 qui est auto-signé. En spécifiant ce fichier concaténé, ça marche avec mon serveur tout comme ça fonctionne aussi avec l’exemple de requête sur ZDS que tu donnes.

Si je donne à tls_set_ca_file un fichier qui n’existe pas, qui est corrompu, ou si je n’appelle pas du tout cette fonction, le programme plante carrément avec un "machin.exe a cessé de fonctionner" que même gdb n’arrive pas à intercepter.

En fait il semblerait que LibreSSL ne va chercher nulle part une banque de certificats racine préétablie par le système. Ou plutôt si, il va dans C:\libreSSL par défaut mais bien sûr c’est un répertoire qui n’a aucune chance d’exister sur le poste de l’utilisateur final. Du coup d’après ce que j’ai compris, le point de départ de la chaîne doit être un certificat auto-signé, et je dois moi-même lui fournir la totalité de la banque à utiliser.

  • En lui donnant Letsencrypt x3 tout seul, ça ne pouvait pas marcher, car le certificat n’est pas auto-signé et on ne trouve pas de variante auto-signée sur le site de Letsencrypt (à juste titre d’après leurs explications)
  • En lui donnant ISRG x1 tout seul ça ne pouvait pas marcher, car dans la chaîne que retourne mon serveur, le certificat Letsencrypt x3 est signé par Digital Signature Trust, et il n’a rien pour reconnaître ce dernier
  • En lui fournissant les deux, il accepte, parce qu’il arrive à s’y retrouver: mon certificat est signé par Letsencrypt, le même certificat de Letsencrypt est signé par quelqu’un d’autre qu’il ne connaît pas mais vu qu’il en a un signé par ISRG qui lui-même est auto-signé et donc considéré comme racine, il est d’accord

Voilà, le sujet est donc résolu ! Merci pour l’aide.

Pour mettre un point final, j’ai changé l’appel à tls_set_ca_file par un appel à tls_set_ca_mem. En clair je lui fais charger les certificats non plus depuis un fichier mais depuis une ressource embarquée à l’intérieur de l’exécutable. Ca compliquera sans doute la vie du petit malin qui pourrait avoir envie de détourner la connexion en refilant un client corrompu.

Après pour être complet et empêcher ça, je devrais faire signer mon exécutable, mais je ne sais pas trop comment faire… je n’ai pas l’impression que ce soit possible sans dépenser une fortune.

Si ça intéresse je peux toujours passer le code final de mes classes Socket et SSLSocket.

+0 -0

Heu ? Je ne vois pas ce qu’il y a de mal ? Ou bien c’est juste que tu na’imes pas le C++ ?

QuentinC

Ne fais pas attention, c’est du troll et une allergie tenace. :-°

En fait il semblerait que LibreSSL ne va chercher nulle part une banque de certificats racine préétablie par le système. Ou plutôt si, il va dans C:\libreSSL par défaut mais bien sûr c’est un répertoire qui n’a aucune chance d’exister sur le poste de l’utilisateur final.

QuentinC

Mmm… Sous OpenBSD, les certificats racines sont tous situés dans le fichier /etc/ssl/cert.pem (que libreSSL utilise par défaut), j’imagine qu’il doit exister un tel fichier ou un répertoire accueillant les certificats sous Windows, non ? Puisque, par exemple, n’importe quel navigateur installé doit pouvoir créer une connexion SSL ou TLS vers un site courant employant HTTPS.

Une fois trouvé, tu devrais déjà disposer de tous les certificats nécessaires et spécifier le fichier système via tls_set_ca_file() ou tls_set_ca_path() si c’est un répertoire. Cela sera à mon sens mieux que d’embarquer ta propre suite de certificats directement dans l’exécutable ou à côté de ce dernier (surtout si celui d’une des CAs est compromis et que celui que tu embarques n’est pas mis à jour).

+0 -0
Connectez-vous pour pouvoir poster un message.
Connexion

Pas encore membre ?

Créez un compte en une minute pour profiter pleinement de toutes les fonctionnalités de Zeste de Savoir. Ici, tout est gratuit et sans publicité.
Créer un compte