Un module que j'ajoute à notre grande application Java doit converser avec le site Web sécurisé par SSL d'une autre société. 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 incorporer ce certificat dans notre code de telle sorte que la connexion au serveur aboutisse.
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 aucun traitement 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 ce seul emplacement dans l'application, et nulle part ailleurs.
Je sais que je peux importer le certificat dans le magasin d'autorités 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; il semble très invasif de 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 d'une autre application Java d'accéder à ce site sont nulles. Ce n'est pas non plus une opération banale: sous UNIX, je dois obtenir les droits d'accès pour modifier le JRE de cette façon.
J'ai également constaté que je pouvais créer une instance TrustManager qui effectue certaines vérifications personnalisées. Il semble que je puisse même être capable de créer un TrustManager qui délègue au réel TrustManager dans tous les cas, sauf celui-ci. Mais il semble que TrustManager soit installé dans le monde entier, et je suppose que cela affecterait toutes les autres connexions de notre application, et cela ne sent pas très bon pour moi non plus.
Quel est le moyen préféré, standard ou optimal de configurer une application Java pour accepter un certificat auto-signé? Puis-je atteindre tous les objectifs que j'ai en tête, ou vais-je devoir faire des compromis? Existe-t-il une option impliquant des fichiers, des répertoires et des paramètres de configuration, ainsi que du code peu à pas?
Créez vous-même une fabrique SSLSocket
et configurez-la sur le HttpsURLConnection
avant de vous connecter.
...
HttpsURLConnection conn = (HttpsURLConnection)url.openConnection();
conn.setSSLSocketFactory(sslFactory);
conn.setMethod("POST");
...
Vous voudrez créer un SSLSocketFactory
et le conserver. Voici un croquis sur la façon de l'initialiser:
/* 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 de format PEM, vous pouvez écrire votre propre code à l'aide de CertificateFactory
ou simplement l'importer avec keytool
à partir du JDK (keytool ne sera pas work pour une "entrée de clé", mais est très bien pour une "entrée de confiance").
keytool -import -file selfsigned.pem -alias server -keystore server.jks
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 contenant le certificat, par exemple:
static public String certificateString=
"-----BEGIN CERTIFICATE-----\n" +
"MIIGQTCCBSmgAwIBAgIHBcg1dAivUzANBgkqhkiG9w0BAQsFADCBjDELMAkGA1UE" +
"BhMCSUwxFjAUBgNVBAoTDVN0YXJ0Q29tIEx0ZC4xKzApBgNVBAsTIlNlY3VyZSBE" +
... a bunch of characters...
"5126sfeEJMRV4Fl2E5W1gDHoOd6V==\n" +
"-----END CERTIFICATE-----";
J'ai testé le fait que vous pouvez insérer 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.
Si la création de SSLSocketFactory
n’est pas une option, il suffit d’importer la clé dans la JVM.
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-----
Importez la clé: #keytool -import -alias dev-server -keystore $Java_HOME/jre/lib/security/cacerts -file dev-server.pem
. Mot de passe: changeit
Redémarrer la machine virtuelle Java
Source: Comment résoudre javax.net.ssl.SSLHandshakeException?
Nous copions le fichier de clés certifiées de JRE et ajoutons nos certificats personnalisés à ce fichier de clés, puis indiquons à l'application d'utiliser le fichier de clés 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, son nouveau fichier de clés certifiées n'est pas automatiquement fusionné avec votre fichier personnalisé.
Vous pourriez peut-être gérer ce scénario en ayant un programme d'installation ou une routine de démarrage qui vérifie le fichier de clés certifiées/jdk et vérifie l'absence de correspondance ou met à jour automatiquement le fichier de clés certifiées. Je ne sais pas ce qui se passe si vous mettez à jour le fichier de clés certifiées pendant l'exécution de l'application.
Cette solution n'est pas à 100% élégante ou infaillible, mais elle est simple, fonctionne et ne nécessite aucun code.
J'ai dû faire quelque chose comme ceci lorsque j'utilisais commons-httpclient pour accéder à un serveur interne https avec un certificat auto-signé. Oui, notre solution consistait à créer un TrustManager personnalisé qui passait tout simplement (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 avoir uniquement notre TrustManager local associé. Vous n'avez pas besoin d'aller près d'un magasin de clés/certstore.
Donc, cela se trouve dans notre LocalSSLSocketocket Factory:
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 implémentant SecureProtocolSocketFactory. LocalSSLTrustManager est l'implémentation du gestionnaire de confiance factice susmentionnée.