Comment puis-je utiliser différents certificats sur des connexions spécifiques?

164

Un module que j'ajoute à notre grande application Java doit converser avec le site Web sécurisé SSL d'une autre entreprise. Le problème est que le site utilise un certificat auto-signé. J'ai une copie du certificat pour vérifier que je ne suis pas confronté à une attaque de type man-in-the-middle, et je dois intégrer ce certificat dans notre code de manière à ce que la connexion au serveur réussisse.

Voici le code de base:

void sendRequest(String dataPacket) {
  String urlStr = "https://host.example.com/";
  URL url = new URL(urlStr);
  HttpURLConnection conn = (HttpURLConnection)url.openConnection();
  conn.setMethod("POST");
  conn.setRequestProperty("Content-Length", data.length());
  conn.setDoOutput(true);
  OutputStreamWriter o = new OutputStreamWriter(conn.getOutputStream());
  o.write(data);
  o.flush();
}

Sans aucune gestion supplémentaire en place pour le certificat auto-signé, cela meurt à conn.getOutputStream () avec l'exception suivante:

Exception in thread "main" 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
....
Caused by: sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
....
Caused by: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target

Idéalement, mon code doit apprendre à Java à accepter ce certificat auto-signé, pour celui-ci dans l'application, et nulle part ailleurs.

Je sais que je peux importer le certificat dans le magasin d'autorité de certification du JRE, ce qui permettra à Java de l'accepter. Ce n'est pas une approche que je veux adopter si je peux aider; cela semble très invasif à faire sur toutes les machines de nos clients pour un module qu'ils ne peuvent pas utiliser; cela affecterait toutes les autres applications Java utilisant le même JRE, et je n'aime pas cela même si les chances pour toute autre application Java d'accéder à ce site sont nulles. Ce n'est pas non plus une opération triviale: sous UNIX, je dois obtenir des droits d'accès pour modifier le JRE de cette manière.

J'ai également vu que je peux créer une instance de TrustManager qui effectue une vérification personnalisée. Il semble que je pourrais même être en mesure de créer un TrustManager qui délègue au vrai TrustManager dans toutes les instances sauf celui-ci. Mais il semble que TrustManager soit installé globalement, et je suppose que cela affecterait toutes les autres connexions de notre application, et cela ne me semble pas tout à fait normal non plus.

Quelle est la méthode préférée, standard ou la meilleure pour configurer une application Java pour accepter un certificat auto-signé? Puis-je atteindre tous les objectifs que j'ai en tête ci-dessus ou vais-je devoir faire des compromis? Existe-t-il une option impliquant des fichiers et des répertoires et des paramètres de configuration, et peu ou pas de code?

sauter
la source
petite solution de travail: rgagnon.com/javadetails
20
@Hasenpriester: veuillez ne pas suggérer cette page. Il désactive toutes les vérifications de confiance. Vous n'allez pas seulement accepter le certificat auto-signé que vous souhaitez, vous acceptez également tout certificat qu'un attaquant MITM vous présentera.
Bruno

Réponses:

168

Créez SSLSocketvous-même une usine et définissez-la sur le HttpsURLConnectionavant de vous connecter.

...
HttpsURLConnection conn = (HttpsURLConnection)url.openConnection();
conn.setSSLSocketFactory(sslFactory);
conn.setMethod("POST");
...

Vous voudrez en créer un SSLSocketFactoryet le conserver. Voici un schéma de l'initialisation:

/* Load the keyStore that includes self-signed cert as a "trusted" entry. */
KeyStore keyStore = ... 
TrustManagerFactory tmf = 
  TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
tmf.init(keyStore);
SSLContext ctx = SSLContext.getInstance("TLS");
ctx.init(null, tmf.getTrustManagers(), null);
sslFactory = ctx.getSocketFactory();

Si vous avez besoin d'aide pour créer le magasin de clés, veuillez commenter.


Voici un exemple de chargement du magasin de clés:

KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
keyStore.load(trustStore, trustStorePassword);
trustStore.close();

Pour créer le magasin de clés avec un certificat au format PEM, vous pouvez écrire votre propre code en utilisant CertificateFactory, ou simplement l'importer avec à keytoolpartir du JDK (keytool ne fonctionnera pas pour une «entrée de clé», mais convient parfaitement pour une «entrée de confiance» ).

keytool -import -file selfsigned.pem -alias server -keystore server.jks
Erickson
la source
3
Merci beaucoup! Les autres gars m'ont aidé à m'orienter dans la bonne direction, mais au final, c'est l'approche que j'ai adoptée. J'ai eu une tâche ardue de convertir le fichier de certificat PEM en fichier de keystore JKS Java, et j'ai trouvé de l'aide pour cela ici: stackoverflow.com/questions/722931/…
skiphoppy
1
Je suis content que ça marche. Je suis désolé que vous ayez eu des difficultés avec le magasin de clés J'aurais dû simplement l'inclure dans ma réponse. Ce n'est pas trop difficile avec CertificateFactory. En fait, je pense que je vais faire une mise à jour pour quiconque viendra plus tard.
erickson
1
Tout ce que ce code fait est de répliquer ce que vous pouvez accomplir en définissant trois propriétés système décrites dans le Guide de référence JSSE.
Marquis de Lorne
2
@EJP: C'était il y a quelque temps, donc je ne m'en souviens pas avec certitude, mais j'imagine que le raisonnement était que dans une "grande application Java", il y a probablement d'autres connexions HTTP établies. La définition des propriétés globales peut interférer avec les connexions fonctionnelles ou permettre à cette seule partie d'usurper les serveurs. Voici un exemple d'utilisation du gestionnaire de confiance intégré au cas par cas.
erickson
2
Comme l'a dit l'OP, "mon code doit apprendre à Java à accepter ce certificat auto-signé, pour celui-ci dans l'application, et nulle part ailleurs."
erickson
18

J'ai lu BEAUCOUP d'endroits en ligne pour résoudre ce problème. Voici le code que j'ai écrit pour le faire fonctionner:

ByteArrayInputStream derInputStream = new ByteArrayInputStream(app.certificateString.getBytes());
CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
X509Certificate cert = (X509Certificate) certificateFactory.generateCertificate(derInputStream);
String alias = "alias";//cert.getSubjectX500Principal().getName();

KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType());
trustStore.load(null);
trustStore.setCertificateEntry(alias, cert);
KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509");
kmf.init(trustStore, null);
KeyManager[] keyManagers = kmf.getKeyManagers();

TrustManagerFactory tmf = TrustManagerFactory.getInstance("X509");
tmf.init(trustStore);
TrustManager[] trustManagers = tmf.getTrustManagers();

SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(keyManagers, trustManagers, null);
URL url = new URL(someURL);
conn = (HttpsURLConnection) url.openConnection();
conn.setSSLSocketFactory(sslContext.getSocketFactory());

app.certificateString est une chaîne qui contient le certificat, par exemple:

static public String certificateString=
        "-----BEGIN CERTIFICATE-----\n" +
        "MIIGQTCCBSmgAwIBAgIHBcg1dAivUzANBgkqhkiG9w0BAQsFADCBjDELMAkGA1UE" +
        "BhMCSUwxFjAUBgNVBAoTDVN0YXJ0Q29tIEx0ZC4xKzApBgNVBAsTIlNlY3VyZSBE" +
        ... a bunch of characters...
        "5126sfeEJMRV4Fl2E5W1gDHoOd6V==\n" +
        "-----END CERTIFICATE-----";

J'ai testé que vous pouvez mettre n'importe quel caractère dans la chaîne de certificat, si elle est auto-signée, à condition de conserver la structure exacte ci-dessus. J'ai obtenu la chaîne de certificat avec la ligne de commande Terminal de mon ordinateur portable.

Josh
la source
1
Merci d'avoir partagé @Josh. J'ai créé un petit projet Github qui montre votre code en cours d'utilisation: github.com/aasaru/ConnectToTrustedServerExample
Master Drools
14

Si la création d'un SSLSocketFactoryn'est pas une option, importez simplement la clé dans la JVM

  1. Récupérez la clé publique :,$openssl s_client -connect dev-server:443 puis créez un fichier dev-server.pem qui ressemble à

    -----BEGIN CERTIFICATE----- 
    lklkkkllklklklklllkllklkl
    lklkkkllklklklklllkllklkl
    lklkkkllklk....
    -----END CERTIFICATE-----
  2. Importer la clé: #keytool -import -alias dev-server -keystore $JAVA_HOME/jre/lib/security/cacerts -file dev-server.pem. Mot de passe: changeit

  3. Redémarrez la JVM

Source: Comment résoudre javax.net.ssl.SSLHandshakeException?

utilisateur454322
la source
1
Je ne pense pas que cela résout la question initiale, mais cela a résolu mon problème, alors merci!
Ed Norris
Ce n'est pas non plus une bonne idée de faire cela: vous avez maintenant ce certificat auto-signé supplémentaire aléatoire à l'échelle du système auquel tous les processus Java feront confiance par défaut.
user268396
12

Nous copions le truststore du JRE et ajoutons nos certificats personnalisés à ce truststore, puis demandons à l'application d'utiliser le truststore personnalisé avec une propriété système. De cette façon, nous laissons le truststore JRE par défaut seul.

L'inconvénient est que lorsque vous mettez à jour le JRE, vous ne fusionnez pas automatiquement son nouveau truststore avec votre personnalisé.

Vous pouvez peut-être gérer ce scénario en ayant un programme d'installation ou une routine de démarrage qui vérifie le truststore / jdk et vérifie une incompatibilité ou met à jour automatiquement le truststore. Je ne sais pas ce qui se passe si vous mettez à jour le truststore pendant que l'application est en cours d'exécution.

Cette solution n'est ni 100% élégante ni infaillible, mais elle est simple, fonctionne et ne nécessite aucun code.

M. brillant et nouveau 安 宇
la source
12

J'ai dû faire quelque chose comme ça lors de l'utilisation de commons-httpclient pour accéder à un serveur https interne avec un certificat auto-signé. Oui, notre solution était de créer un TrustManager personnalisé qui passait simplement tout (journalisation d'un message de débogage).

Cela revient à avoir notre propre SSLSocketFactory qui crée des sockets SSL à partir de notre SSLContext local, qui est configuré pour n'avoir que notre TrustManager local associé. Vous n'avez pas du tout besoin de vous approcher d'un keystore / certstore.

C'est donc dans notre LocalSSLSocketFactory:

static {
    try {
        SSL_CONTEXT = SSLContext.getInstance("SSL");
        SSL_CONTEXT.init(null, new TrustManager[] { new LocalSSLTrustManager() }, null);
    } catch (NoSuchAlgorithmException e) {
        throw new RuntimeException("Unable to initialise SSL context", e);
    } catch (KeyManagementException e) {
        throw new RuntimeException("Unable to initialise SSL context", e);
    }
}

public Socket createSocket(String host, int port) throws IOException, UnknownHostException {
    LOG.trace("createSocket(host => {}, port => {})", new Object[] { host, new Integer(port) });

    return SSL_CONTEXT.getSocketFactory().createSocket(host, port);
}

Avec d'autres méthodes mettant en œuvre SecureProtocolSocketFactory. LocalSSLTrustManager est l'implémentation du gestionnaire de confiance factice susmentionnée.

araqnid
la source
8
Si vous désactivez toutes les vérifications de confiance, il est inutile d'utiliser SSL / TLS en premier lieu. C'est OK pour tester localement, mais pas si vous souhaitez vous connecter à l'extérieur.
Bruno
J'obtiens cette exception lors de son exécution sur Java 7. javax.net.ssl.SSLHandshakeException: aucune suite de chiffrement en commun Pouvez-vous m'aider?
Uri Lukach