TLS (Transport Layer Security), plus souvent dénommé SSL (Secured Sockets Layer - son prédécesseur déprécié), est l'un des protocoles les plus utilisés sur le Web pour sécuriser les sites et les applications qui le peuplent. TLS repose à la fois sur des algorithmes de cryptographie asymétrique (clé publique et clé privée) et une relation de confiance avec des autorités reconnues à travers le monde, en se basant sur un système de certificats sécurisés.

Cependant, ce modèle ne fonctionne pas lorsque vous êtes enfermé derrière le proxy d'une entreprise, qui ne souhaite pas (à raison ?) que les applications qu'elle développe se mettent à transmettre (par erreur, dans le meilleur des cas !) des informations confidentielles ou qu'elles soient des portes ouvertes pour la plus grande joie des mauvaises intentions du Net 😱

Néanmoins, à l'intérieur de cette entreprise, comme il est nécessaire de sécuriser la communication sur le réseau, il faut passer par TLS. Mais cette fois, l'entreprise va se déclarer elle-même autorité de certificat, en exposant en interne un certificat dit "auto-signé". C'est là que les ennuis commencent !

1/ Vous lancez Maven ou SBT... Et boom ! ça crashe, parce que ça ne passe pas le pare-feu. On vous dit alors de passer par un proxy Maven (Nexus ou Artifactory).

2/ Vous configurez vos outils. Vous retentez... Et boom ! Vous avez une exception à peine compréhensible qui parle de certification, de Sun et de PKIX. On vous dit alors qu'il y a un vague JDK, qui a été correctement configuré avec tous les certificats et qui traine quelque part sur le réseau interne.

3/ Vous récupérez et installez ce JDK. Vous retentez... Et boom ! Vous obtenez la même erreur que la dernière fois. À ce moment là, vous commencez à avoir du mal à faire la distinction entre TLS et la magie. Et vous êtes sur le point de friser la folie 🤯

Reproduction du cas

Commençons par générer un certificat auto-signé que nous allons placer dans un keystore, nommé ici selfsigned.jks.

$ keytool -genkey \\
    -alias selfsigned -keyalg RSA \\
    -keypass changeit -storepass changeit \\
    -keystore tmp/certificate/selfsigned.jks

What is your first and last name?
  [Unknown]:  François Sarradin
What is the name of your organizational unit?
  [Unknown]:
What is the name of your organization?
  [Unknown]:  Univalence
What is the name of your City or Locality?
  [Unknown]:  Paris
What is the name of your State or Province?
  [Unknown]:
What is the two-letter country code for this unit?
  [Unknown]:  fr
Is
  CN=François Sarradin, OU=Unknown, O=Univalence, L=Paris, ST=Unknown, C=fr
correct?
  [no]:  yes

Nous allons utiliser ce keystore pour monter un faux service Web sécurisé. Pour cela nous créons d'abord un contexte SSL, à travers une fonction qui prend en paramètres le chemin vers ce keystore ainsi que le mot de passe associé.

import java.io.FileInputStream
import java.security.KeyStore
import javax.net.ssl.{KeyManagerFactory, SSLContext, TrustManagerFactory}

def createSSLContext(keystorePath: String, password: String): SSLContext = {
    val keyStore = KeyStore.getInstance("JKS")
    keyStore.load(new FileInputStream(keystorePath), password.toCharArray)

    val keyManagerFactory = KeyManagerFactory.getInstance("SunX509")
    keyManagerFactory.init(keyStore, password.toCharArray)
    val keyManagers = keyManagerFactory.getKeyManagers

    val trustManagerFactory = TrustManagerFactory.getInstance("SunX509")
    trustManagerFactory.init(keyStore)
    val trustManagers = trustManagerFactory.getTrustManagers

    val sslContext = SSLContext.getInstance("TLS")
    sslContext.init(keyManagers, trustManagers, null)

    sslContext
  }

Il faut ensuite créer une configuration pour le service Web avec ce contexte.

import com.sun.net.httpserver._

    val httpsConfigurator =
      new HttpsConfigurator(sslContext) { self =>
        override def configure(params: HttpsParameters): Unit = {
          val c = self.getSSLContext
          val engine = c.createSSLEngine
          params.setNeedClientAuth(false)
          params.setCipherSuites(engine.getEnabledCipherSuites)
          params.setProtocols(engine.getEnabledProtocols)
          params.setSSLParameters(c.getDefaultSSLParameters)
        }
      }

Et on lance le service.

import com.sun.net.httpserver._
import java.net.InetSocketAddress

    val server = HttpsServer.create(new InetSocketAddress("0.0.0.0", 9443), 0)
    server.setHttpsConfigurator(httpsConfigurator)
    server.createContext("/", (exchange: HttpExchange) => {
      val content = "Hello"
      val length = content.length
      val raw = content.getBytes

      exchange.sendResponseHeaders(200, length)
      exchange.getResponseBody.write(raw)

      exchange.close()
    })
    server.start()

On va maintenant se créer un client Web.

import java.net.URL

object Main {
  def main(args: Array[String]): Unit = {
    val result = new URL("<https://127.0.0.1:9443/>").getContent()

    println(result)
  }
}

En lançant le service Web, puis le client, nous obtenons alors l'exception ci-dessous.

Exception in thread "main" javax.net.ssl.SSLHandshakeException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
	at java.base/sun.security.ssl.Alert.createSSLException(Alert.java:128)
	at java.base/sun.security.ssl.TransportContext.fatal(TransportContext.java:321)
	at java.base/sun.security.ssl.TransportContext.fatal(TransportContext.java:264)
	at java.base/sun.security.ssl.TransportContext.fatal(TransportContext.java:259)
	at java.base/sun.security.ssl.CertificateMessage$T13CertificateConsumer.checkServerCerts(CertificateMessage.java:1329)
	at java.base/sun.security.ssl.CertificateMessage$T13CertificateConsumer.onConsumeCertificate(CertificateMessage.java:1204)
	at java.base/sun.security.ssl.CertificateMessage$T13CertificateConsumer.consume(CertificateMessage.java:1151)
	at java.base/sun.security.ssl.SSLHandshake.consume(SSLHandshake.java:392)
	at java.base/sun.security.ssl.HandshakeContext.dispatch(HandshakeContext.java:444)
	at java.base/sun.security.ssl.HandshakeContext.dispatch(HandshakeContext.java:421)
	at java.base/sun.security.ssl.TransportContext.dispatch(TransportContext.java:178)
	at java.base/sun.security.ssl.SSLTransport.decode(SSLTransport.java:164)
	at java.base/sun.security.ssl.SSLSocketImpl.decode(SSLSocketImpl.java:1152)
	at java.base/sun.security.ssl.SSLSocketImpl.readHandshakeRecord(SSLSocketImpl.java:1063)
	at java.base/sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:402)
	at java.base/sun.net.www.protocol.https.HttpsClient.afterConnect(HttpsClient.java:567)
	at java.base/sun.net.www.protocol.https.AbstractDelegateHttpsURLConnection.connect(AbstractDelegateHttpsURLConnection.java:185)
	at java.base/sun.net.www.protocol.http.HttpURLConnection.getInputStream0(HttpURLConnection.java:1581)
	at java.base/sun.net.www.protocol.http.HttpURLConnection.getInputStream(HttpURLConnection.java:1509)
	at java.base/java.net.URLConnection.getContent(URLConnection.java:749)
	at java.base/sun.net.www.protocol.https.HttpsURLConnectionImpl.getContent(HttpsURLConnectionImpl.java:425)
	at java.base/java.net.URL.getContent(URL.java:1131)
	at io.univalence.test_ssl.Main$.main(Main.scala:7)
	at io.univalence.test_ssl.Main.main(Main.scala)
Caused by: sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
	at java.base/sun.security.validator.PKIXValidator.doBuild(PKIXValidator.java:385)
	at java.base/sun.security.validator.PKIXValidator.engineValidate(PKIXValidator.java:290)
	at java.base/sun.security.validator.Validator.validate(Validator.java:264)
	at java.base/sun.security.ssl.X509TrustManagerImpl.validate(X509TrustManagerImpl.java:321)
	at java.base/sun.security.ssl.X509TrustManagerImpl.checkTrusted(X509TrustManagerImpl.java:221)
	at java.base/sun.security.ssl.X509TrustManagerImpl.checkServerTrusted(X509TrustManagerImpl.java:129)
	at java.base/sun.security.ssl.CertificateMessage$T13CertificateConsumer.checkServerCerts(CertificateMessage.java:1313)
	... 19 more
Caused by: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
	at java.base/sun.security.provider.certpath.SunCertPathBuilder.build(SunCertPathBuilder.java:141)
	at java.base/sun.security.provider.certpath.SunCertPathBuilder.engineBuild(SunCertPathBuilder.java:126)
	at java.base/java.security.cert.CertPathBuilder.build(CertPathBuilder.java:297)
	at java.base/sun.security.validator.PKIXValidator.doBuild(PKIXValidator.java:380)
	... 25 more

Analyse