Accepter le certificat SSL auto-signé du serveur dans le client Java

215

Cela ressemble à une question standard, mais je n'ai trouvé aucune direction claire nulle part.

J'ai du code java essayant de me connecter à un serveur avec un certificat probablement auto-signé (ou expiré). Le code signale l'erreur suivante:

[HttpMethodDirector] I/O exception (javax.net.ssl.SSLHandshakeException) caught 
when processing request: sun.security.validator.ValidatorException: PKIX path 
building failed: sun.security.provider.certpath.SunCertPathBuilderException: 
unable to find valid certification path to requested target

Si je comprends bien, je dois utiliser keytool et dire à java que c'est OK pour autoriser cette connexion.

Toutes les instructions pour résoudre ce problème supposent que je suis parfaitement compétent avec keytool, comme

générer une clé privée pour le serveur et l'importer dans le magasin de clés

Y a-t-il quelqu'un qui pourrait publier des instructions détaillées?

J'utilise unix, donc le script bash serait le meilleur.

Je ne sais pas si c'est important, mais le code est exécuté dans jboss.

Nikita Rybak
la source
2
Voir Comment puis-je accepter un certificat auto-signé avec une connexion Java HttpsURLConnection? . De toute évidence, il serait préférable que le site utilise un certificat valide.
Matthew Flaschen
2
Merci pour le lien, je ne l'ai pas vu lors de la recherche. Mais les deux solutions impliquent un code spécial pour envoyer une demande et j'utilise du code existant (client amazon ws pour java). Respectivement, c'est leur site que je connecte et je ne peux pas résoudre ses problèmes de certificat.
Nikita Rybak
2
@MatthewFlaschen - "De toute évidence, il serait préférable que le site utilise un certificat valide ..." - Un certificat auto-signé est un certificat valide si le client lui fait confiance. Beaucoup pensent que conférer la confiance au cartel CA / Navigateur est un défaut de sécurité.
jww
3
En relation, voir Le code le plus dangereux du monde: validation des certificats SSL dans un logiciel non navigateur . (Le lien est fourni car vous semblez obtenir ces réponses de spam qui désactivent la validation).
jww

Réponses:

307

Vous avez essentiellement deux options ici: ajoutez le certificat auto-signé à votre magasin de clés de confiance JVM ou configurez votre client pour

Option 1

Exportez le certificat à partir de votre navigateur et importez-le dans votre magasin de confiance JVM (pour établir une chaîne de confiance):

<JAVA_HOME>\bin\keytool -import -v -trustcacerts
-alias server-alias -file server.cer
-keystore cacerts.jks -keypass changeit
-storepass changeit 

Option 2

Désactiver la validation du certificat:

// Create a trust manager that does not validate certificate chains
TrustManager[] trustAllCerts = new TrustManager[] { 
    new X509TrustManager() {     
        public java.security.cert.X509Certificate[] getAcceptedIssuers() { 
            return new X509Certificate[0];
        } 
        public void checkClientTrusted( 
            java.security.cert.X509Certificate[] certs, String authType) {
            } 
        public void checkServerTrusted( 
            java.security.cert.X509Certificate[] certs, String authType) {
        }
    } 
}; 

// Install the all-trusting trust manager
try {
    SSLContext sc = SSLContext.getInstance("SSL"); 
    sc.init(null, trustAllCerts, new java.security.SecureRandom()); 
    HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());
} catch (GeneralSecurityException e) {
} 
// Now you can access an https URL without having the certificate in the truststore
try { 
    URL url = new URL("https://hostname/index.html"); 
} catch (MalformedURLException e) {
} 

Notez que je ne recommande pas du tout l'option n ° 2 . La désactivation du gestionnaire de confiance annule certaines parties de SSL et vous rend vulnérable aux attaques de l'homme au milieu. Préférez l'option n ° 1 ou, mieux encore, demandez au serveur d'utiliser un "vrai" certificat signé par une autorité de certification bien connue.

Pascal Thivent
la source
7
Pas seulement l'attaque MIM. Cela vous rend vulnérable à la connexion au mauvais site. C'est complètement précaire. Voir RFC 2246. Je m'oppose à la publication de ce TrustManager à tout moment. Ce n'est même pas correct par rapport à ses propres spécifications.
Marquis de Lorne
10
@EJP Je ne recommande vraiment pas la deuxième option (j'ai mis à jour ma réponse pour que ce soit clair). Cependant, ne pas l'afficher ne résoudra rien (il s'agit d'informations publiques) et IMHO ne mérite pas un downvote.
Pascal Thivent
135
@EJP C'est en enseignant aux gens que vous les éduquez, pas en cachant des choses. Donc, garder les choses secrètes ou dans l'obscurité n'est pas du tout une solution. Ce code est public, l'API Java est publique, il vaut mieux en parler que de l'ignorer. Mais je peux vivre avec toi sans être d'accord.
Pascal Thivent
10
L'autre option - celle que vous ne mentionnez pas - consiste à faire réparer le certificat du serveur soit en le réparant vous-même, soit en appelant les personnes de support concernées. Les certificats d'hôte unique sont vraiment très bon marché; fouiner avec des trucs auto-signés est insensé (pour ceux qui ne connaissent pas cet idiome anglais, un ensemble de priorités totalement stupide qui coûte beaucoup pour économiser presque rien).
Donal Fellows
2
@Rich, 6 ans de retard, mais vous pouvez également obtenir le certificat du serveur dans Firefox en cliquant sur l'icône de verrouillage -> plus d'informations -> Afficher le certificat -> onglet Détails -> Exporter ... Il est bien caché.
Siddhartha
9

J'ai pourchassé ce problème à un fournisseur de certificats qui ne fait pas partie des hôtes de confiance JVM par défaut JDK 8u74. Le fournisseur est www.identrust.com , mais ce n'était pas le domaine auquel j'essayais de me connecter. Ce domaine avait obtenu son certificat de ce fournisseur. Voir Est-ce que la couverture cross root est approuvée par la liste par défaut dans le JDK / JRE? - lisez quelques entrées. Voir également Quels navigateurs et systèmes d'exploitation prennent en charge Let's Encrypt .

Donc, afin de me connecter au domaine qui m'intéressait, qui avait un certificat émis par, identrust.comj'ai fait les étapes suivantes. Fondamentalement, je devais obtenir le identrust.com (DST Root CA X3 certificat ) pour être approuvé par la JVM. J'ai pu le faire en utilisant Apache HttpComponents 4.5 comme ceci:

1: Obtenez le certificat d'indettrust dans les instructions de téléchargement de la chaîne de certificats . Cliquez sur le lien DST Root CA X3 .

2: enregistrez la chaîne dans un fichier nommé "DST Root CA X3.pem". Assurez-vous d'ajouter les lignes "----- BEGIN CERTIFICATE -----" et "----- END CERTIFICATE -----" dans le fichier au début et à la fin.

3: Créez un fichier de clés Java, cacerts.jks avec la commande suivante:

keytool -import -v -trustcacerts -alias IdenTrust -keypass yourpassword -file dst_root_ca_x3.pem -keystore cacerts.jks -storepass yourpassword

4: Copiez le fichier de clés cacerts.jks résultant dans le répertoire des ressources de votre application java / (maven).

5: Utilisez le code suivant pour charger ce fichier et le joindre à Apache 4.5 HttpClient. Cela résoudra le problème pour tous les domaines pour lesquels des certificats émis par indetrust.comutil oracle incluent le certificat dans le magasin de clés par défaut JRE.

SSLContext sslcontext = SSLContexts.custom()
        .loadTrustMaterial(new File(CalRestClient.class.getResource("/cacerts.jks").getFile()), "yourpasword".toCharArray(),
                new TrustSelfSignedStrategy())
        .build();
// Allow TLSv1 protocol only
SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(
        sslcontext,
        new String[] { "TLSv1" },
        null,
        SSLConnectionSocketFactory.getDefaultHostnameVerifier());
CloseableHttpClient httpclient = HttpClients.custom()
        .setSSLSocketFactory(sslsf)
        .build();

Lorsque le projet est généré, le fichier cacerts.jks sera copié dans le chemin de classe et chargé à partir de là. Je n'ai pas, à ce stade, testé contre d'autres sites SSL, mais si le code ci-dessus "chaînes" dans ce certificat alors ils fonctionneront aussi, mais encore une fois, je ne sais pas.

Référence: contexte SSL personnalisé et comment puis-je accepter un certificat auto-signé avec une connexion Java HttpsURLConnection?

K.Nicholas
la source
La question concerne un certificat auto-signé. Cette réponse ne l'est pas.
Marquis de Lorne
4
Lisez la question (et la réponse) un peu plus près.
K.Nicholas
9

Apache HttpClient 4.5 prend en charge l'acceptation de certificats auto-signés:

SSLContext sslContext = SSLContexts.custom()
    .loadTrustMaterial(new TrustSelfSignedStrategy())
    .build();
SSLConnectionSocketFactory socketFactory =
    new SSLConnectionSocketFactory(sslContext);
Registry<ConnectionSocketFactory> reg =
    RegistryBuilder.<ConnectionSocketFactory>create()
    .register("https", socketFactory)
    .build();
HttpClientConnectionManager cm = new PoolingHttpClientConnectionManager(reg);        
CloseableHttpClient httpClient = HttpClients.custom()
    .setConnectionManager(cm)
    .build();
HttpGet httpGet = new HttpGet(url);
CloseableHttpResponse sslResponse = httpClient.execute(httpGet);

Cela crée une fabrique de sockets SSL qui utilisera le TrustSelfSignedStrategy , l'enregistre auprès d'un gestionnaire de connexions personnalisé puis effectue un HTTP GET à l'aide de ce gestionnaire de connexions.

Je suis d'accord avec ceux qui chantent "ne faites pas cela en production", mais il existe des cas d'utilisation pour accepter des certificats auto-signés en dehors de la production; nous les utilisons dans des tests d'intégration automatisés, de sorte que nous utilisons SSL (comme en production) même lorsqu'ils ne fonctionnent pas sur le matériel de production.

effrayant
la source
6

Plutôt que de définir la fabrique de socket par défaut (laquelle IMO est une mauvaise chose) - cela affectera simplement la connexion actuelle plutôt que chaque connexion SSL que vous essayez d'ouvrir:

URLConnection connection = url.openConnection();
    // JMD - this is a better way to do it that doesn't override the default SSL factory.
    if (connection instanceof HttpsURLConnection)
    {
        HttpsURLConnection conHttps = (HttpsURLConnection) connection;
        // Set up a Trust all manager
        TrustManager[] trustAllCerts = new TrustManager[] { new X509TrustManager()
        {

            public java.security.cert.X509Certificate[] getAcceptedIssuers()
            {
                return null;
            }

            public void checkClientTrusted(
                java.security.cert.X509Certificate[] certs, String authType)
            {
            }

            public void checkServerTrusted(
                java.security.cert.X509Certificate[] certs, String authType)
            {
            }
        } };

        // Get a new SSL context
        SSLContext sc = SSLContext.getInstance("TLSv1.2");
        sc.init(null, trustAllCerts, new java.security.SecureRandom());
        // Set our connection to use this SSL context, with the "Trust all" manager in place.
        conHttps.setSSLSocketFactory(sc.getSocketFactory());
        // Also force it to trust all hosts
        HostnameVerifier allHostsValid = new HostnameVerifier() {
            public boolean verify(String hostname, SSLSession session) {
                return true;
            }
        };
        // and set the hostname verifier.
        conHttps.setHostnameVerifier(allHostsValid);
    }
InputStream stream = connection.getInputStream();
Jon Daniel
la source
2
Votre checkServerTrustedn'implémente pas la logique nécessaire pour réellement approuver le certificat et vous assurer que les certificats non approuvés sont rejetés. Cela pourrait faire une bonne lecture: Le code le plus dangereux du monde: valider les certificats SSL dans un logiciel non navigateur .
jww
1
Votre getAcceptedIssuers()méthode n'est pas conforme à la spécification, et cette «solution» reste radicalement précaire.
Marquis de Lorne
4

Il existe une meilleure alternative à l'approbation de tous les certificats: créez un TrustStorequi approuve spécifiquement un certificat donné et utilisez-le pour créer un à SSLContextpartir duquel obtenir le SSLSocketFactorypour définir sur le HttpsURLConnection. Voici le code complet:

File crtFile = new File("server.crt");
Certificate certificate = CertificateFactory.getInstance("X.509").generateCertificate(new FileInputStream(crtFile));

KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
keyStore.load(null, null);
keyStore.setCertificateEntry("server", certificate);

TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
trustManagerFactory.init(keyStore);

SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, trustManagerFactory.getTrustManagers(), null);

HttpsURLConnection connection = (HttpsURLConnection) new URL(url).openConnection();
connection.setSSLSocketFactory(sslContext.getSocketFactory());

Vous pouvez également charger le KeyStoredirectement à partir d'un fichier ou récupérer le certificat X.509 à partir de n'importe quelle source fiable.

Notez qu'avec ce code, les certificats cacertsne seront pas utilisés. Ce particulier HttpsURLConnectionne fera confiance qu'à ce certificat spécifique.

Johannes Brodwall
la source
1

Faites confiance à tous les certificats SSL: - Vous pouvez contourner SSL si vous souhaitez tester sur le serveur de test. Mais n'utilisez pas ce code pour la production.

public static class NukeSSLCerts {
protected static final String TAG = "NukeSSLCerts";

public static void nuke() {
    try {
        TrustManager[] trustAllCerts = new TrustManager[] { 
            new X509TrustManager() {
                public X509Certificate[] getAcceptedIssuers() {
                    X509Certificate[] myTrustedAnchors = new X509Certificate[0];  
                    return myTrustedAnchors;
                }

                @Override
                public void checkClientTrusted(X509Certificate[] certs, String authType) {}

                @Override
                public void checkServerTrusted(X509Certificate[] certs, String authType) {}
            }
        };

        SSLContext sc = SSLContext.getInstance("SSL");
        sc.init(null, trustAllCerts, new SecureRandom());
        HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());
        HttpsURLConnection.setDefaultHostnameVerifier(new HostnameVerifier() {
            @Override
            public boolean verify(String arg0, SSLSession arg1) {
                return true;
            }
        });
    } catch (Exception e) { 
    }
}

}

Veuillez appeler cette fonction dans la fonction onCreate () dans Activity ou dans votre classe d'application.

NukeSSLCerts.nuke();

Cela peut être utilisé pour Volley dans Android.

Ashish Saini
la source
Pas tout à fait sûr de la raison pour laquelle vous êtes rejeté, à moins que le code ne fonctionne pas. C'est peut-être l'hypothèse qu'Android est en quelque sorte impliqué lorsque la question d'origine n'a pas marqué Android.
Lo-Tan
1

La réponse acceptée est correcte, mais je voudrais ajouter quelque chose à cela car j'utilisais IntelliJ sur Mac et je ne pouvais pas le faire fonctionner en utilisant la JAVA_HOMEvariable path.

Il s'avère que Java Home était différent lors de l'exécution de l'application à partir d'IntelliJ.

Pour savoir exactement où il se trouve, vous pouvez simplement faire System.getProperty("java.home")comme c'est là que les certificats de confiance sont lus.

Christopher Schneider
la source
0

S'ils utilisent un certificat auto-signé, c'est à eux de prendre les mesures nécessaires pour rendre leur serveur utilisable. Plus précisément, cela signifie vous fournir leur certificat hors ligne de manière fiable. Alors, faites-les faire ça. Vous importez ensuite cela dans votre fichier de clés certifiées à l'aide de l'outil de clés comme décrit dans le Guide de référence JSSE. Ne pensez même pas au TrustManager non sécurisé publié ici.

EDIT Pour le bénéfice des dix - sept (!) Downvoters, et de nombreux commentateurs ci-dessous, qui n'ont clairement pas réellement lu ce que j'ai écrit ici, ce n'est pas une jérémiade contre les certificats auto-signés. Il n'y a rien de mal avec les certificats auto-signés lorsqu'ils sont correctement mis en œuvre. Mais, la bonne façon de les implémenter est de faire livrer le certificat en toute sécurité via un processus hors ligne, plutôt que via le canal non authentifié qu'ils vont être utilisés pour s'authentifier. C'est sûrement évident? C'est certainement évident pour chaque organisation soucieuse de la sécurité pour laquelle j'ai déjà travaillé, des banques avec des milliers de succursales à mes propres entreprises. La «solution» de base de code côté client de la confiance tousles certificats, y compris les certificats auto-signés signés par absolument n'importe qui, ou tout organisme arbitral s'établissant en tant qu'AC, ne sont ipso facto pas sécurisés. C'est juste jouer à la sécurité. C'est inutile. Vous avez une conversation privée, inviolable, à l'épreuve des réponses et à l'épreuve des injections avec ... quelqu'un. N'importe qui. Un homme au milieu. Un imitateur. N'importe qui. Vous pouvez tout aussi bien utiliser du texte en clair.

Marquis de Lorne
la source
2
Le fait qu'un serveur ait décidé d'utiliser https ne signifie pas que la personne avec le client se moque de la sécurité à ses propres fins.
Gus
3
Je suis surpris que cette réponse ait été autant rejetée. J'aimerais comprendre un peu plus pourquoi. Il semble qu'EJP suggère que vous ne devriez pas faire l'option 2 car elle permet une faille de sécurité majeure. Quelqu'un pourrait-il expliquer pourquoi (autres que les paramètres de pré-déploiement) pourquoi cette réponse est de faible qualité?
cr1pto
2
Eh bien, je suppose que tout cela. Que signifie "il leur appartient de prendre les mesures nécessaires pour rendre leur serveur utilisable" ? Que doit faire un opérateur de serveur pour le rendre utilisable? Quelles sont les étapes que vous envisagez? Pouvez-vous en fournir une liste? Mais en revenant à 1 000 pieds, comment cela est-il lié au problème posé par le PO? Il veut savoir comment faire accepter au client le certificat auto-signé en Java. C'est une simple question de confier la confiance au code puisque l'OP trouve le certificat acceptable.
jww
2
@EJP - Corrigez-moi si je me trompe, mais accepter un certificat auto-signé est une décision de politique côté client. Cela n'a rien à voir avec les opérations côté serveur. Le client doit prendre la décision. Les parités doivent travailler ensemble pour résoudre le problème de distribution clé, mais cela n'a rien à voir avec le fait de rendre le serveur utilisable.
jww
3
@EJP - Je n'ai certainement pas dit "faites confiance à tous les certificats" . Peut-être que je manque quelque chose (ou que vous lisez trop dans les choses) ... L'OP a le certificat, et c'est acceptable pour lui. Il veut savoir comment lui faire confiance. Je divise probablement les cheveux, mais le certificat n'a pas besoin d'être livré hors ligne. Une vérification hors bande doit être utilisée, mais ce n'est pas la même chose que "doit être remis au client hors ligne" . Il pourrait même être le magasin produisant le serveur et le client, il a donc les connaissances a priori requises .
2017
0

Au lieu d'utiliser keytool comme suggéré par le commentaire du haut, sur RHEL, vous pouvez utiliser update-ca-trust à partir des versions plus récentes de RHEL 6. Vous devrez avoir le certificat au format pem. ensuite

trust anchor <cert.pem>

Modifiez /etc/pki/ca-trust/source/cert.p11-kit et remplacez "catégorie de certificat: autre entrée" par "catégorie de certificat: autorité". (Ou utilisez sed pour le faire dans un script.)

update-ca-trust

Quelques mises en garde:

  • Je n'ai pas pu trouver "confiance" sur mon serveur RHEL 6 et yum n'a pas proposé de l'installer. J'ai fini par l'utiliser sur un serveur RHEL 7 et à copier le fichier .p11-kit.
  • Pour que cela fonctionne pour vous, vous devrez peut-être le faire update-ca-trust enable. Cela remplacera / etc / pki / java / cacerts par un lien symbolique pointant vers / etc / pki / ca-trust / extract / java / cacerts. (Donc, vous voudrez peut-être sauvegarder l'ancien en premier.)
  • Si votre client java utilise des cacerts stockés dans un autre emplacement, vous voudrez le remplacer manuellement par un lien symbolique vers / etc / pki / ca-trust / extract / java / cacerts, ou le remplacer par ce fichier.
user66660
la source
0

J'ai eu le problème que je passais une URL dans une bibliothèque qui appelait url.openConnection();j'ai adapté la réponse de jon-daniel,

public class TrustHostUrlStreamHandler extends URLStreamHandler {

    private static final Logger LOG = LoggerFactory.getLogger(TrustHostUrlStreamHandler.class);

    @Override
    protected URLConnection openConnection(final URL url) throws IOException {

        final URLConnection urlConnection = new URL(url.getProtocol(), url.getHost(), url.getPort(), url.getFile()).openConnection();

        // adapated from
        // /programming/2893819/accept-servers-self-signed-ssl-certificate-in-java-client
        if (urlConnection instanceof HttpsURLConnection) {
            final HttpsURLConnection conHttps = (HttpsURLConnection) urlConnection;

            try {
                // Set up a Trust all manager
                final TrustManager[] trustAllCerts = new TrustManager[] { new X509TrustManager() {

                    @Override
                    public java.security.cert.X509Certificate[] getAcceptedIssuers() {
                        return null;
                    }

                    @Override
                    public void checkClientTrusted(final java.security.cert.X509Certificate[] certs, final String authType) {
                    }

                    @Override
                    public void checkServerTrusted(final java.security.cert.X509Certificate[] certs, final String authType) {
                    }
                } };

                // Get a new SSL context
                final SSLContext sc = SSLContext.getInstance("TLSv1.2");
                sc.init(null, trustAllCerts, new java.security.SecureRandom());
                // Set our connection to use this SSL context, with the "Trust all" manager in place.
                conHttps.setSSLSocketFactory(sc.getSocketFactory());
                // Also force it to trust all hosts
                final HostnameVerifier allHostsValid = new HostnameVerifier() {
                    @Override
                    public boolean verify(final String hostname, final SSLSession session) {
                        return true;
                    }
                };

                // and set the hostname verifier.
                conHttps.setHostnameVerifier(allHostsValid);

            } catch (final NoSuchAlgorithmException e) {
                LOG.warn("Failed to override URLConnection.", e);
            } catch (final KeyManagementException e) {
                LOG.warn("Failed to override URLConnection.", e);
            }

        } else {
            LOG.warn("Failed to override URLConnection. Incorrect type: {}", urlConnection.getClass().getName());
        }

        return urlConnection;
    }

}

En utilisant cette classe, il est possible de créer une nouvelle URL avec:

trustedUrl = new URL(new URL(originalUrl), "", new TrustHostUrlStreamHandler());
trustedUrl.openConnection();

Cela a l'avantage d'être localisé et de ne pas remplacer la valeur par défaut URL.openConnection .

David Ryan
la source