Certificats auto-signé et communication SSL en Java

Le développeur Java utilisant l’API java.net se retrouvera tôt ou tard confronté à un cas récurrent : l’accès à un service en SSL. Attention je ne parle pas uniquement de HTTPS mais de tous les protocoles ayant une déclinaison SSL : IMAPS, POP3S, SMTPS, LDAPS, … les cas ne manquent pas.

À partir de ce moment le développeur se trouve en face de 2 situations :

  • Le serveur dispose d’une certificat à jour et émis par une autorité de certification connue par la JVM. Tout se passe normalement en utilisant la SSLSocketFactory fournit en standard.
  • Le certificat du serveur distant ne répond pas à ces caractéristiques. Dans l’immense majorité des cas il s’agit de certificats auto-signés. À ce moment, l’API java.net vous refusera la connexion à ce serveur en lançant une SSLHandshakeException souvent noyée dans une pile d’exécution incompréhensible.

Pourquoi ce comportement? On touche là aux fondations de la signature numérique : la chaîne de confiance. Si un tiers n’est reconnu par aucun élément de la chaîne de confiance, l’identification est impossible. Dans notre cas d’utilisation cela se traduit par l’arrêt pur et simple des communications par l’API java.net.

Pourtant, le certificat auto-signé se justifie complètement dans certains cas d’utilisation, dans un environnement de développement par exemple. Pour faire face à ce problème, 2 méthodes s’offrent au développeur Java :

  • Faire reconnaître à la JVM notre tiers en tant que tiers de confiance.
  • Faire preuve de laxisme.

Mais si l’objectif recherché est le même, chacune des méthodes agit à des niveaux différents et présente à ce titre des caractérisques singulières qu’il faudra prendre en considération.

 

Importer un nouveau tiers de confiance

Pour sa gestion interne, la JVM maintient une liste de clés et de certificats : le keystore. Il s’agit d’une base de donnée interne, accessible à toutes les applications. Son fonctionnement est identique à celui que l’on retrouve dans les navigateurs Internet :

  • Une liste d’autorité de certification triée sur le volet est maintenue.
  • Lors d’une demande de connexion SSL, le certificat tiers est confronté à la liste. Si le tiers est approuvé la procédure se poursuit. Dans le cas contraire, le processus est arrêté et l’information remontée.

1ère étape : récupérer le certificat du serveur

Plusieurs possibilités s’offrent à vous à ce niveau : wget, curl, votre navigateur internet préféré. Pour ma part, j’utilise openssl pour récupérer la clé publique du site https://google.fr – le certificat n’est pas reconnu par votre navigateur car délivré pour google.com –.

$ openssl s_client  -connect google.fr:443

CONNECTED(00000003)
depth=1 /C=ZA/O=Thawte Consulting (Pty) Ltd./CN=Thawte SGC CA
verify error:num=20:unable to get local issuer certificate
verify return:0
---
Certificate chain
 0 s:/C=US/ST=California/L=Mountain View/O=Google Inc/CN=www.google.com
   i:/C=ZA/O=Thawte Consulting (Pty) Ltd./CN=Thawte SGC CA
 1 s:/C=ZA/O=Thawte Consulting (Pty) Ltd./CN=Thawte SGC CA
   i:/C=US/O=VeriSign, Inc./OU=Class 3 Public Primary Certification Authority
---
Server certificate
-----BEGIN CERTIFICATE-----
MIIDITCCAoqgAwIBAgIQPI06ZO4Y3RtzC6GS7viYGzANBgkqhkiG9w0BAQUFADBM

MQswCQYDVQQGEwJaQTElMCMGA1UEChMcVGhhd3RlIENvbnN1bHRpbmcgKFB0eSkg
THRkLjEWMBQGA1UEAxMNVGhhd3RlIFNHQyBDQTAeFw0wODA1MDIxNzAyNTVaFw0w
OTA1MDIxNzAyNTVaMGgxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlh
MRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRMwEQYDVQQKEwpHb29nbGUgSW5jMRcw
FQYDVQQDEw53d3cuZ29vZ2xlLmNvbTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkC
gYEAmxntXaVWr0lm23n9whx4Tk8RpYqs4pTu4+JLwAMlp5nMZeHslK6u8KeZvBDX
7YcwR81Q+a/T0/QLjUeKLuLOU5uRmX8eXPkb1umTZ+NK+M/EjAxo0ZdURw4KJDCn
gpSu3q4/v7oUxviykI42reHQvhaas15yOEnadKE//9KHge0CAwEAAaOB5zCB5DAo
BgNVHSUEITAfBggrBgEFBQcDAQYIKwYBBQUHAwIGCWCGSAGG+EIEATA2BgNVHR8E
LzAtMCugKaAnhiVodHRwOi8vY3JsLnRoYXd0ZS5jb20vVGhhd3RlU0dDQ0EuY3Js
MHIGCCsGAQUFBwEBBGYwZDAiBggrBgEFBQcwAYYWaHR0cDovL29jc3AudGhhd3Rl
LmNvbTA+BggrBgEFBQcwAoYyaHR0cDovL3d3dy50aGF3dGUuY29tL3JlcG9zaXRv
cnkvVGhhd3RlX1NHQ19DQS5jcnQwDAYDVR0TAQH/BAIwADANBgkqhkiG9w0BAQUF
AAOBgQAxCmyinulUGRZomZHWQ8trtMxszLD78e6BvwArb1ASxq8CKjbBKN7FTFYg
bfU9QrkYgSCy3Vdd674yhFBFUW7N5C4qOIifUu0o//yNV7WtZK5NDg7ZPay4/mZM
FY9EUvp8PATtfzdhBP7V6bmwnv6lEWnJY9ZGgW8A2HIvgjdEwQ==
-----END CERTIFICATE-----
subject=/C=US/ST=California/L=Mountain View/O=Google Inc/CN=www.google.com
issuer=/C=ZA/O=Thawte Consulting (Pty) Ltd./CN=Thawte SGC CA
---
No client certificate CA names sent
---
SSL handshake has read 1765 bytes and written 304 bytes
---
New, TLSv1/SSLv3, Cipher is RC4-SHA
Server public key is 1024 bit
Compression: NONE
Expansion: NONE
SSL-Session:
    Protocol  : TLSv1
    Cipher    : RC4-SHA
    Session-ID: 726C5364E699DDD61F8D04E1D6604C3E798BBA93753BA6E9028A9BDBE2CC0D1F
    Session-ID-ctx:
    Master-Key: 34CD7AA6E15D66F4BB29D3D128C1A50DA9567EF663BE5AE13B0F7FEB66325078D1299D0A14AE90BA231BDBF947789A6B
    Key-Arg   : None
    Start Time: 1224514678
    Timeout   : 300 (sec)
    Verify return code: 20 (unable to get local issuer certificate)
---
read:errno=0

La commande ne rendant pas la main directement, il vous faudra casser la communication avec CTRL+C. La partie qui nous intéresse ici celle qui démarre avec —–BEGIN CERTIFICATE—– et se termine avec —–END CERTIFICATE—– :

-----BEGIN CERTIFICATE-----
MIIDITCCAoqgAwIBAgIQPI06ZO4Y3RtzC6GS7viYGzANBgkqhkiG9w0BAQUFADBM
MQswCQYDVQQGEwJaQTElMCMGA1UEChMcVGhhd3RlIENvbnN1bHRpbmcgKFB0eSkg
THRkLjEWMBQGA1UEAxMNVGhhd3RlIFNHQyBDQTAeFw0wODA1MDIxNzAyNTVaFw0w
OTA1MDIxNzAyNTVaMGgxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlh
MRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRMwEQYDVQQKEwpHb29nbGUgSW5jMRcw
FQYDVQQDEw53d3cuZ29vZ2xlLmNvbTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkC
gYEAmxntXaVWr0lm23n9whx4Tk8RpYqs4pTu4+JLwAMlp5nMZeHslK6u8KeZvBDX
7YcwR81Q+a/T0/QLjUeKLuLOU5uRmX8eXPkb1umTZ+NK+M/EjAxo0ZdURw4KJDCn
gpSu3q4/v7oUxviykI42reHQvhaas15yOEnadKE//9KHge0CAwEAAaOB5zCB5DAo
BgNVHSUEITAfBggrBgEFBQcDAQYIKwYBBQUHAwIGCWCGSAGG+EIEATA2BgNVHR8E
LzAtMCugKaAnhiVodHRwOi8vY3JsLnRoYXd0ZS5jb20vVGhhd3RlU0dDQ0EuY3Js
MHIGCCsGAQUFBwEBBGYwZDAiBggrBgEFBQcwAYYWaHR0cDovL29jc3AudGhhd3Rl
LmNvbTA+BggrBgEFBQcwAoYyaHR0cDovL3d3dy50aGF3dGUuY29tL3JlcG9zaXRv
cnkvVGhhd3RlX1NHQ19DQS5jcnQwDAYDVR0TAQH/BAIwADANBgkqhkiG9w0BAQUF
AAOBgQAxCmyinulUGRZomZHWQ8trtMxszLD78e6BvwArb1ASxq8CKjbBKN7FTFYg
bfU9QrkYgSCy3Vdd674yhFBFUW7N5C4qOIifUu0o//yNV7WtZK5NDg7ZPay4/mZM
FY9EUvp8PATtfzdhBP7V6bmwnv6lEWnJY9ZGgW8A2HIvgjdEwQ==
-----END CERTIFICATE----

La clé publique est au format PEM. Vous pouvez l’enregistrer dans un fichier telle quelle, google.pub pour mon cas.

2ème étape : importation dans le keystore

Par défaut, le keystore de la JVM se situe dans le fchier JAVA_HOME/jre/lib/security/cacerts. Pour le manipuler, vous pouvez utiliser l’outil keytool ou des outils plus accessibles comme KeyTool IUI.

Listons d’abord une première fois son contenue :

$ keytool -v -list -keystore /usr/lib/jvm/java-6-sun/jre/lib/security/cacerts
Enter keystore password:  changeit

Keystore type: jks
Keystore provider: SUN

Your keystore contains 52 entries

Alias name: aolrootca1
Creation date: Jan 17, 2008
Entry type: trustedCertEntry

Owner: CN=America Online Root Certification Authority 1, O=America Online Inc., C=US
Issuer: CN=America Online Root Certification Authority 1, O=America Online Inc., C=US
Serial number: 1
Valid from: Tue May 28 08:00:00 CEST 2002 until: Thu Nov 19 21:43:00 CET 2037
Certificate fingerprints:
     MD5:  14:F1:08:AD:9D:FA:64:E2:89:E7:1C:CF:A8:AD:7D:5E
     SHA1: 39:21:C1:15:C1:5D:0E:CA:5C:CB:5B:C4:F0:7D:21:D8:05:0B:56:6A


*******************************************
*******************************************
......

Par défaut le mot de passe du keystore est changeit. Ce sera manifestement votre cas si vous arrivez ici. Il ne vous reste plus qu’à importer la clé publique récupérée précédemment

$ sudo keytool -import -keystore /usr/lib/jvm/java-6-sun/jre/lib/security/cacerts -alias google  -file google.pub
Enter keystore password:  changeit
Owner: CN=www.google.com, O=Google Inc, L=Mountain View, ST=California, C=US
Issuer: CN=Thawte SGC CA, O=Thawte Consulting (Pty) Ltd., C=ZA
Serial number: 3c8d3a64ee18dd1b730ba192eef8981b
Valid from: Fri May 02 19:02:55 CEST 2008 until: Sat May 02 19:02:55 CEST 2009
Certificate fingerprints:
     MD5:  63:1E:F3:56:B0:B0:F7:8D:E4:8C:8F:7D:8E:F5:68:D0
     SHA1: 8A:AA:9A:71:F0:5C:E7:25:8A:35:0A:32:B1:91:69:44:9B:36:93:A8
Trust this certificate? [no]:  yes
Certificate was added to keystore

Attention : l’opération peut échouer si l’utilisateur n’a pas accès en écriture au fichier du keystore. À partir de ce moment, le certificat fraîchement importé sera reconnu par tous composants utilisant le keystore Java même s’il n’est pas issu d’une autorité de certification reconnue.

 

Être laxiste

Il s’agit ici d’étendre le mécanisme de création des sockets SSL. 2 classes de l’API java.net sont ici importantes :

  • La première, javax.net.ssl.SSLSocketFactory, est responsable de la création des sockets dans un environnement SSL. Pour cela, la fabrique a besoin de se baser sur une implémentation de TrustManager pour effectuer la vérification des tiers.
  • C’est javax.net.ssl.X509TrustManager qui effectue la vérification du tiers – le serveur distant – en soumettant le certificat du serveur au keystore utilisé. Le comportement par défaut rejettera, entre autres, toutes connexions avec un serveur utilisant un certificat auto-signé ou émis par une autorité de certificat inconnue par le keystore. C’est cette implémentation qui est utilisée par défaut.

1ère étape : implémenter X509TrustManager

package fr.ippon.example

import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;

import javax.net.ssl.X509TrustManager;

public class LazyTrustManager implements X509TrustManager {
    public boolean isClientTrusted(X509Certificate[] cert) {
        return true;
    }
    public boolean isServerTrusted(X509Certificate[] cert) {
        return true;
    }
    public X509Certificate[] getAcceptedIssuers() {
        return new X509Certificate[0];
    }
    public void checkClientTrusted(X509Certificate[] arg0, String arg1)
            throws CertificateException {}
    public void checkServerTrusted(X509Certificate[] arg0, String arg1)
            throws CertificateException {}
}

L’implémentation de notre LazyTrustManager se résume ici à sa plus simple expression : approuver tout le monde.

2ème étape : étendre SSLSocketFactory

package fr.ippon.example

import java.io.IOException;
import java.net.InetAddress;
import java.net.Socket;

import javax.net.SocketFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;

public class LazySSLSocketFactory extends SSLSocketFactory {
	private SSLSocketFactory factory;
	public LazySSLSocketFactory() {
		try {
			SSLContext sslcontext = SSLContext.getInstance("TLS");
			sslcontext.init(
					null, // No KeyManager required
					new TrustManager[] { new LazyTrustManager() },
					new java.security.SecureRandom());
			factory = (SSLSocketFactory) sslcontext.getSocketFactory();
		} catch (Exception ex) {
			ex.printStackTrace();
		}
	}
	public static SocketFactory getDefault() {
		return new LazySSLSocketFactory();
	}
	public Socket createSocket() throws IOException {
		return factory.createSocket();
	}
	public Socket createSocket(Socket socket, String s, int i, boolean flag)
			throws IOException {
		return factory.createSocket(socket, s, i, flag);
	}
	public Socket createSocket(InetAddress inaddr, int i, InetAddress inaddr1,
			int j) throws IOException {
		return factory.createSocket(inaddr, i, inaddr1, j);
	}
	public Socket createSocket(InetAddress inaddr, int i) throws IOException {
		return factory.createSocket(inaddr, i);
	}
	public Socket createSocket(String s, int i, InetAddress inaddr, int j)
			throws IOException {
		return factory.createSocket(s, i, inaddr, j);
	}
	public Socket createSocket(String s, int i) throws IOException {
		return factory.createSocket(s, i);
	}
	public String[] getDefaultCipherSuites() {
		return factory.getSupportedCipherSuites();
	}
	public String[] getSupportedCipherSuites() {
		return factory.getSupportedCipherSuites();
	}
}

Notre LazySSLSocketFactory a donc 2 caractéristiques importantes :

  • Elle utilise notre LazyTrustManager qui, pour rappel, approuve tout le monde.
  • Elle utilise la SSLSocketFactory par défaut pour la création du socket.

3ème étape : utilisation

Rien de très compliqué à ce niveau : on référence notre LazySSLSocketFactory AVANT la création du socket, et a fortiori avant utilisation de l’API utilisé (JavaMAIL, HTTPClient, ….)

Security.setProperty("ssl.SocketFactory.provider",LazySSLSocketFactory.class.getName());

 

TIPS : pour ceux qui utilise une méthode similaire avec JavaMail, il faut s’assurer que la méthode SSLSocketFactory.createSocket() est bien surchargée par votre implémentation. Dans le cas contraire, vous vous retrouvez sans doute avec l’exception suivante :

javax.mail.MessagingException: Unconnected sockets not implemented;
  nested exception is:
    java.net.SocketException: Unconnected sockets not implemented
    at com.sun.mail.imap.IMAPStore.protocolConnect(IMAPStore.java:571)

Que choisir?

Finalement, tout dépend essentiellement de l’environnement dans lequel on se trouve.

On choisira l’utilisation du keystore  :

  • lorsqu’on se trouve en production et que le tiers est fautif mais qu’on ne peut rien y faire…
  • lorsque l’on veut pouvoir gérer de manière fine les approbations (les serveurs d’application proposent d’ailleurs ce type de fonctionnalité dans leur interface d’administration).

Notez cependant que l’utilisation du keystore impacte toutes les applications utilisant la JVM.

On préfèrera la seconde méthode  :

  • pour son côté portable.
  • d’une manière générale, lors de développement d’API d’accès ou de logiciels offrant la possibilité de s’affranchir de cette contrainte.
Tweet about this on TwitterShare on FacebookGoogle+Share on LinkedIn

10 réflexions au sujet de « Certificats auto-signé et communication SSL en Java »

  1. A noter qu'au lieu d'importer le certifiicat en question au keystore de la jvm, il est souvent préférable d'indiquer à notre application un autre keystore qu'on aura pu générer spécialement pour l'occasion et contenant uniquement le certificat dont on a besoin.Pour prendre un cas concret, lorsque le portail Liferay doit s'interfacer avec un CAS tournant en https, on peut créer un keystore particulier contenant le certificat du CAS en question et préciser à notre serveur l'emplacement du keystore à prendre en compte via l'option de jvm suivante :JAVA_OPTS= … -Djavax.net.ssl.trustStore=/path/to/keystoreCela évite de "polluer" le keystore de la jvm.

  2. Merci pour ce tutorialMais j'ai des problèmes avec chacune de ces méthodes (je travail sous Android). keyStore, J'ai une IO Exception quand je charge mon fichier dans le keystore ( wrong version of keystore )La seconde méthode pose des problèmes de cast imposibles entre les librairies Apache et Javax

    1. Android et Java ce n'est pas la même chose et l'on ne pas tout appliquer comme ça sans modification. Visiblement Android utilise un composant Apache – org.apache.http.conn.ssl.SSLSocketFactory – pour la gestion des communications SSL. Il faut voir comment l'utiliser. Pour ma part j'ai l'impression que ça ne s'utilise out-of-the-box qu'avec HttpClient:Pour le wrong version of keystore peut être que d'Android ne supporte pas le format de clé utilisé? 

  3. Pendant mes recherches je suis aussi tombé sur cet article. L'auteur a fait une classe java qui permet de tester en SSL l'url passé en argument et propose d'installer les certificats renvoyés par le serveur. Très pratique pour éviter de manipuler les outils précédemments cités.

  4. Très bon tuto Pierre-Alain.

    ça m'a enlevé une épine du peid…

    J'ai utilisé la 2ème solution je n'ai plus une erreur de ssl, mais par contre j'ai un warning:

    WARN [ServiceDelegateImpl] Cannot access wsdlURL: <a href="https://sfg-bpf-preprod.servicesgp.mpsa.com/fr/services/ServicePSAGF_Dealer.wsdlhttps://sfg-bpf-preprod.servicesgp.mpsa.com/fr/se… />

    18:25:04,326 INFO [STDOUT] Exception: Cannot get port meta data for: com.inetpsa.xml.services.sfg.service_dealer.ServicePSAGFDealerPortType

    ça a peut être un rapport avec le ssl ou c'est peut être simplement un problème de compile.

    Mais j'ai pensé que quelqu'un aurait une idée.

    Merci.

     

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *


*