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 LazySSLSocketFactoryAVANT 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.