Java prend-il en charge les certificats Let's Encrypt?

125

Je développe une application Java qui interroge une API REST sur un serveur distant via HTTP. Pour des raisons de sécurité, cette communication doit être commutée sur HTTPS.

Maintenant que Let's Encrypt a commencé sa version bêta publique, j'aimerais savoir si Java fonctionne actuellement (ou s'il est confirmé qu'il fonctionnera à l'avenir) avec leurs certificats par défaut.

Let's Encrypt a obtenu sa signature croisée intermédiaire par IdenTrust , ce qui devrait être une bonne nouvelle. Cependant, je ne trouve aucun de ces deux dans la sortie de cette commande:

keytool -keystore "..\lib\security\cacerts" -storepass changeit -list

Je sais que des autorités de certification de confiance peuvent être ajoutées manuellement sur chaque machine, mais comme mon application doit être téléchargeable gratuitement et exécutable sans autre configuration, je recherche des solutions qui fonctionnent «hors de la boîte». Avez-vous de bonnes nouvelles pour moi?

Hexaholic
la source
1
On peut également vérifier la compatibilité de Let's Encrypt ici letsencrypt.org/docs/certificate-compatibility
potame
@potame "avec Java 8u131, vous devez toujours ajouter votre certificat à votre truststore" donc si vous obtenez un certificat de Let's Encrypt, vous devrez ajouter le certificat que vous avez obtenu au truststore? Ne devrait-il pas suffire que leur CA soit inclus?
mxro
1
@mxro Salut - merci d'avoir attiré mon attention là-dessus. Mes commentaires ci-dessus ne sont pas du tout vrais (en fait le problème était plus compliqué que cela et lié à notre infrastructure) et je vais les supprimer car ils ne conduisent en effet qu'à la confusion. Donc, si vous avez un jdk> Java 8u101, le certificat Let's Encrypt devrait fonctionner et être correctement reconnu et approuvé.
potame
@potame C'est excellent. Merci pour la clarification!
mxro

Réponses:

142

[ Mise à jour du 08/06/2016 : selon https://bugs.openjdk.java.net/browse/JDK-8154757, l'autorité de certification IdenTrust sera incluse dans Oracle Java 8u101.]

[ Mise à jour du 05/08/2016 : Java 8u101 a été publié et inclut effectivement IdenTrust CA: notes de version ]


Java prend-il en charge les certificats Let's Encrypt?

Oui. Le certificat Let's Encrypt n'est qu'un certificat de clé publique ordinaire. Java le prend en charge (selon la compatibilité des certificats Let's Encrypt , pour Java 7> = 7u111 et Java 8> = 8u101).

Java fait-il confiance aux certificats Let's Encrypt prêts à l'emploi?

Non / cela dépend de la JVM. Le truststore d'Oracle JDK / JRE jusqu'à 8u66 ne contient ni l'AC Let's Encrypt spécifiquement, ni l'AC IdenTrust qui l'a signé de manière croisée. new URL("https://letsencrypt.org/").openConnection().connect();par exemple, il en résulte javax.net.ssl.SSLHandshakeException: sun.security.validator.ValidatorException.

Vous pouvez toutefois fournir votre propre validateur / définir un fichier de clés personnalisé contenant l'autorité de certification racine requise ou importer le certificat dans le magasin de clés de confiance JVM.

https://community.letsencrypt.org/t/will-the-cross-root-cover-trust-by-the-default-list-in-the-jdk-jre/134/10 aborde également le sujet.


Voici un exemple de code qui montre comment ajouter un certificat au truststore par défaut au moment de l'exécution. Vous aurez juste besoin d'ajouter le certificat (exporté depuis Firefox en tant que .der et mis dans classpath)

Basé sur Comment puis-je obtenir une liste de certificats racine de confiance en Java? et http://developer.android.com/training/articles/security-ssl.html#UnknownCa

import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.net.URLConnection;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.KeyStore;
import java.security.cert.Certificate;
import java.security.cert.CertificateFactory;
import java.security.cert.PKIXParameters;
import java.security.cert.TrustAnchor;
import java.security.cert.X509Certificate;

import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLHandshakeException;
import javax.net.ssl.TrustManagerFactory;

public class SSLExample {
    // BEGIN ------- ADDME
    static {
        try {
            KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
            Path ksPath = Paths.get(System.getProperty("java.home"),
                    "lib", "security", "cacerts");
            keyStore.load(Files.newInputStream(ksPath),
                    "changeit".toCharArray());

            CertificateFactory cf = CertificateFactory.getInstance("X.509");
            try (InputStream caInput = new BufferedInputStream(
                    // this files is shipped with the application
                    SSLExample.class.getResourceAsStream("DSTRootCAX3.der"))) {
                Certificate crt = cf.generateCertificate(caInput);
                System.out.println("Added Cert for " + ((X509Certificate) crt)
                        .getSubjectDN());

                keyStore.setCertificateEntry("DSTRootCAX3", crt);
            }

            if (false) { // enable to see
                System.out.println("Truststore now trusting: ");
                PKIXParameters params = new PKIXParameters(keyStore);
                params.getTrustAnchors().stream()
                        .map(TrustAnchor::getTrustedCert)
                        .map(X509Certificate::getSubjectDN)
                        .forEach(System.out::println);
                System.out.println();
            }

            TrustManagerFactory tmf = TrustManagerFactory
                    .getInstance(TrustManagerFactory.getDefaultAlgorithm());
            tmf.init(keyStore);
            SSLContext sslContext = SSLContext.getInstance("TLS");
            sslContext.init(null, tmf.getTrustManagers(), null);
            SSLContext.setDefault(sslContext);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
    // END ---------- ADDME

    public static void main(String[] args) throws IOException {
        // signed by default trusted CAs.
        testUrl(new URL("https://google.com"));
        testUrl(new URL("https://www.thawte.com"));

        // signed by letsencrypt
        testUrl(new URL("https://helloworld.letsencrypt.org"));
        // signed by LE's cross-sign CA
        testUrl(new URL("https://letsencrypt.org"));
        // expired
        testUrl(new URL("https://tv.eurosport.com/"));
        // self-signed
        testUrl(new URL("https://www.pcwebshop.co.uk/"));

    }

    static void testUrl(URL url) throws IOException {
        URLConnection connection = url.openConnection();
        try {
            connection.connect();
            System.out.println("Headers of " + url + " => "
                    + connection.getHeaderFields());
        } catch (SSLHandshakeException e) {
            System.out.println("Untrusted: " + url);
        }
    }

}
zapl
la source
Une dernière chose: quel fichier dois-je utiliser? Let's Encrypt propose un certificat racine et quatre certificats intermédiaires à télécharger. J'ai essayé isrgrootx1.deret lets-encrypt-x1-cross-signed.der, mais aucun d'eux ne semble être le bon.
Hexaholic
@Hexaholic Dépend de ce à quoi vous voulez faire confiance et de la configuration des certificats du site que vous utilisez. Allez https://helloworld.letsencrypt.orgpar exemple sur et inspectez la chaîne de certificats dans le navigateur (en cliquant sur l'icône verte). Pour celui-ci, vous avez besoin soit du certificat spécifique au site, soit le certificat intermédiaire X1 (signé par IdenTrust) ou le certificat DSTRootCAX3. Le ISRG Root X1ne fonctionne pas pour le site helloworld car il n'est pas dans la chaîne, c'est la chaîne alternative. J'utiliserais le DSTRoot, simplement exporté via un navigateur car je ne l'ai vu nulle part en téléchargement.
zapl
1
Merci pour le code. N'oubliez pas de fermer InputStream dans keyStore.load (Files.newInputStream (ksPath)).
Michael Wyraz
3
@ adapt-dev Non, pourquoi un logiciel devrait-il faire confiance à un système inconnu pour fournir de bons certificats? Le logiciel doit pouvoir faire confiance aux certificats auxquels il fait confiance. Ce serait une faille de sécurité si cela signifiait que mon programme java pourrait installer des certificats pour un autre programme. Mais cela ne se produit pas ici, le code n'ajoute le certificat que pendant son propre runtime
zapl
3
+1 pour afficher du code qui ne triche pas seulement en définissant la javax.net.ssl.trustStorepropriété système, mais -1 pour ensuite définir la valeur par défaut de la JVM SSLContext. Il serait préférable d'en créer un nouveau SSLSocketFactoryet de l'utiliser ensuite pour diverses connexions (par exemple HttpUrlConnection) plutôt que de remplacer la configuration SSL à l'échelle de la VM. (Je me rends compte que ce changement n'est efficace que pour la JVM en cours d'exécution et ne persiste pas ou n'affecte pas les autres programmes. Je pense simplement que c'est une meilleure pratique de programmation d'être explicite sur l'endroit où votre configuration s'applique.)
Christopher Schultz
65

Je sais que l'OP a demandé une solution sans modifications de configuration locale, mais au cas où vous voudriez ajouter la chaîne de confiance au keystore de manière permanente:

$ keytool -trustcacerts \
    -keystore $JAVA_HOME/jre/lib/security/cacerts \
    -storepass changeit \
    -noprompt \
    -importcert \
    -file /etc/letsencrypt/live/hostname.com/chain.pem

source: https://community.letsencrypt.org/t/will-the-cross-root-cover-trust-by-the-default-list-in-the-jdk-jre/134/13

Jan Berkel
la source
6
Oui, l'OP ne l'a pas demandé, mais c'était de loin la solution la plus simple pour moi!
Auspex
J'ai importé ce certificat letsencrypt.org/certs/lets-encrypt-x3-cross-signed.pem.txt dans mon ancienne version java 8 et le service Web qui utilise le certificat lets encrypt n'est pas accessible! Cette solution était simple et rapide.
ezwrighter
56

Réponse détaillée pour ceux d'entre nous souhaitant apporter des modifications de configuration locales qui incluent la sauvegarde du fichier de configuration:

1. Testez si cela fonctionne avant les changements

Si vous n'avez pas encore de programme de test, vous pouvez utiliser mon programme java SSLPing ping qui teste la négociation TLS (fonctionnera avec n'importe quel port SSL / TLS, pas seulement HTTPS). J'utiliserai le SSLPing.jar prédéfini, mais lire le code et le construire vous-même est une tâche rapide et facile:

$ git clone https://github.com/dimalinux/SSLPing.git
Cloning into 'SSLPing'...
[... output snipped ...]

Comme ma version Java est antérieure à 1.8.0_101 (non publiée au moment de la rédaction de cet article), un certificat Let's Encrypt ne sera pas vérifié par défaut. Voyons à quoi ressemble l'échec avant d'appliquer le correctif:

$ java -jar SSLPing/dist/SSLPing.jar helloworld.letsencrypt.org 443
About to connect to 'helloworld.letsencrypt.org' on port 443
javax.net.ssl.SSLHandshakeException: sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
[... output snipped ...]

2. Importez le certificat

Je suis sous Mac OS X avec le jeu de variables d'environnement JAVA_HOME. Les commandes ultérieures supposeront que cette variable est définie pour l'installation java que vous modifiez:

$ echo $JAVA_HOME 
/Library/Java/JavaVirtualMachines/jdk1.8.0_92.jdk/Contents/Home/

Faites une sauvegarde du fichier cacerts que nous allons modifier afin que vous puissiez annuler toute modification sans réinstaller le JDK:

$ sudo cp -a $JAVA_HOME/jre/lib/security/cacerts $JAVA_HOME/jre/lib/security/cacerts.orig

Téléchargez le certificat de signature que nous devons importer:

$ wget https://letsencrypt.org/certs/lets-encrypt-x3-cross-signed.der

Effectuez l'importation:

$ sudo keytool -trustcacerts -keystore $JAVA_HOME/jre/lib/security/cacerts -storepass changeit -noprompt -importcert -alias lets-encrypt-x3-cross-signed -file lets-encrypt-x3-cross-signed.der 
Certificate was added to keystore

3. Vérifiez qu'il fonctionne après les modifications

Vérifiez que Java est maintenant bien connecté au port SSL:

$ java -jar SSLPing/dist/SSLPing.jar helloworld.letsencrypt.org 443
About to connect to 'helloworld.letsencrypt.org' on port 443
Successfully connected
dimalinux
la source
8

Pour le JDK qui ne prend pas encore en charge les certificats Let's Encrypt, vous pouvez les ajouter au JDK en cacertssuivant ce processus (grâce à cela ).

Téléchargez tous les certificats sur https://letsencrypt.org/certificates/ (choisissez le format der ) et ajoutez-les un par un avec ce type de commande (exemple pour letsencryptauthorityx1.der):

keytool -import -keystore PATH_TO_JDK\jre\lib\security\cacerts -storepass changeit -noprompt -trustcacerts -alias letsencryptauthorityx1 -file PATH_TO_DOWNLOADS\letsencryptauthorityx1.der
Anthony O.
la source
Cela améliore la situation, mais j'obtiens une erreur de connexion: javax.net.ssl.SSLException: java.lang.RuntimeException: impossible de générer une paire de clés DH
nafg