Wie kann ich verschiedene Zertifikate für bestimmte Verbindungen verwenden?


147

Ein Modul, das ich zu unserer großen Java-Anwendung hinzufüge, muss sich mit der SSL-gesicherten Website eines anderen Unternehmens unterhalten. Das Problem besteht darin, dass die Site ein selbstsigniertes Zertifikat verwendet. Ich habe eine Kopie des Zertifikats, um zu verifizieren, dass ich keinen Man-in-the-Middle-Angriff erlebe, und ich muss dieses Zertifikat so in unseren Code einbinden, dass die Verbindung zum Server erfolgreich ist.

Hier ist der Grundcode:

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(); 
} 

Ohne zusätzliche Behandlung an Ort und Stelle für das selbst signiertes Zertifikat, das bei conn.getOutputStream stirbt() mit folgenden Ausnahme:

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 

Idealer Mein Code muss Java beibringen, dieses selbstsignierte Zertifikat zu akzeptieren, für diesen einen Punkt in der Anwendung und nirgendwo sonst.

Ich weiß, dass ich das Zertifikat in den Zertifikatsautoritätsspeicher der JRE importieren kann, und das erlaubt Java, es zu akzeptieren. Das ist kein Ansatz, den ich machen möchte, wenn ich helfen kann. es scheint sehr invasiv zu sein, auf allen Maschinen unserer Kunden für ein Modul zu arbeiten, das sie nicht benutzen können; Es würde alle anderen Java-Anwendungen betreffen, die dieselbe JRE verwenden, und das mag ich nicht, obwohl die Wahrscheinlichkeit, dass eine andere Java-Anwendung jemals auf diese Site zugreift, gleich Null ist. Es ist auch keine triviale Operation: Unter UNIX muss ich Zugriffsrechte erhalten, um die JRE auf diese Weise zu modifizieren.

Ich habe auch gesehen, dass ich eine TrustManager-Instanz erstellen kann, die einige benutzerdefinierte Überprüfung durchführt. Es sieht so aus, als könnte ich sogar einen TrustManager erstellen, der in allen Instanzen mit Ausnahme dieses einen Zertifikats an den echten TrustManager delegiert. Aber es sieht so aus, als ob TrustManager global installiert wird, und ich nehme an, dass dies Auswirkungen auf alle anderen Verbindungen unserer Anwendung haben würde, und das riecht auch nicht richtig für mich.

Was ist der bevorzugte, standardmäßige oder beste Weg, eine Java-Anwendung so einzurichten, dass ein selbstsigniertes Zertifikat akzeptiert wird? Kann ich alle oben genannten Ziele erreichen, oder muss ich Kompromisse eingehen? Gibt es eine Option für Dateien und Verzeichnisse und Konfigurationseinstellungen und wenig bis gar keinen Code?

  0

kleine, Arbeitsfix: http://www.rgagnon.com/javadetails/java-fix-certificate-problem-in-HTTPS.html 16 aug. 112011-08-16 14:06:49

+16

@Hasenpriester: bitte nicht diese Seite vorschlagen. Es deaktiviert die Vertrauensverifizierung. Sie akzeptieren nicht nur das selbstsignierte Zertifikat, das Sie möchten, sondern Sie akzeptieren auch jedes Zertifikat, das ein MITM-Angreifer Ihnen präsentiert. 23 dez. 112011-12-23 13:23:14

150

Erstellen Sie eine SSLSocket Fabrik selbst, und stellen Sie sie auf die HttpsURLConnection vor dem Anschließen ein.

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

Sie wollen erstellen SSLSocketFactory und halten Sie sie um.Hier ist eine Skizze, wie man es initialisiert:

Wenn Sie Hilfe benötigen, den Schlüsselspeicher zu schaffen, kommentieren Sie bitte.


Hier ist ein Beispiel für den Schlüsselspeicher geladen:

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

den Schlüsselspeicher mit einem PEM-Format Zertifikat zu erstellen, können Sie Ihren eigenen Code verwenden CertificateFactory, schreiben oder einfach importieren mit keytool aus das JDK (keytool wird nicht arbeiten für einen "Schlüsseleintrag", aber ist gut für einen "vertrauenswürdigen Eintrag").

keytool -import -file selfsigned.pem -alias server -keystore server.jks 
+3

Vielen Dank! Die anderen Jungs haben mir geholfen, in die richtige Richtung zu zeigen, aber am Ende ist dein Ansatz der richtige. Ich hatte eine anstrengende Aufgabe, die PEM-Zertifikatsdatei in eine JKS-Java-Keystore-Datei umzuwandeln, und ich fand Hilfe dafür: http://stackoverflow.com/questions/722931/ssl-socket-php-code-needs-to- be-converted-to-java 13 mai. 092009-05-13 21:37:13

+1

Ich bin froh, dass es geklappt hat. Es tut mir leid, dass du mit dem Schlüsselspeicher gekämpft hast. Ich hätte es nur in meine Antwort aufnehmen sollen. Mit CertificateFactory ist das nicht schwer. Tatsächlich denke ich, dass ich für alle, die später kommen, ein Update machen werde. 13 mai. 092009-05-13 21:40:51

  0

Wow. Die Umwandlung von PEM in JKS ist wesentlich einfacher als das, was ich getan habe! Vielen Dank! :) 14 mai. 092009-05-14 19:19:00

+1

All dieser Code repliziert, was Sie erreichen können, indem Sie drei Systemeigenschaften festlegen, die im JSSE-Referenzhandbuch beschrieben sind. 31 mai. 122012-05-31 19:36:53

+1

@EJP: Das war vor einer Weile, also kann ich mich nicht sicher erinnern, aber ich vermute, dass die Gründe darin liegen, dass in einer "großen Java-Anwendung" wahrscheinlich andere HTTP-Verbindungen eingerichtet werden. Durch das Festlegen globaler Eigenschaften können funktionierende Verbindungen beeinträchtigt werden oder diese Partei kann Server fälschen. Dies ist ein Beispiel für die Verwendung des integrierten Trust-Managers von Fall zu Fall. 31 mai. 122012-05-31 22:26:58

+2

Wie das OP sagte, "muss mein Code Java beibringen, dieses selbstsignierte Zertifikat zu akzeptieren, für diesen einen Punkt in der Anwendung und nirgendwo sonst." 31 mai. 122012-05-31 22:29:09

  0

Ich habe versucht, Ihre Lösung ohne Erfolg zu implementieren bitte helfen Sie mir http://StackOverflow.com/Questions/20190364/How-to-Bypass-CertificateException-by-Java 25 nov. 132013-11-25 10:59:37

  0

Wenn Sie wollten, dass es "global" akzeptiert wird keytool, um das Zertifikat als vertrauenswürdiges Zertifikat in der cacerts-Datei zu installieren. Dann wird für jede gestartete VM, die diese JRE verwendet, vertraut. 06 dez. 132013-12-06 19:59:08

  0

@KyleM Das ist kein guter Ansatz für die globale Akzeptanz. 06 dez. 132013-12-06 20:03:33

  0

Deshalb habe ich "global" in Anführungszeichen gesetzt und erklärt, wie es tatsächlich funktioniert. Niemand hat es so genannt, obwohl es eine sehr verbreitete und akzeptierte Methode ist. 06 dez. 132013-12-06 20:07:48

  0

Viele große Organisationen akzeptieren diese Methode nicht. Es wäre ein Sicherheitsverstoß mit schwerwiegenden Folgen. 06 dez. 132013-12-06 21:09:37

  0

@erickson Hallo - es ist vier Jahre später und der Ansatz, den du uns beigebracht hast, hat gut mitgesummt und wurde für verschiedene Server benutzt, mit denen wir kommunizieren. Jetzt habe ich einen neuen Trick, den ich herausfinden muss (kann eine neue StackOverflow-Frage dafür stellen): Ich möchte sowohl dem Standard-Schlüsselspeicher als auch dem benutzerdefinierten Schlüsselspeicher mit dem selbstsignierten Zertifikat des Servers vertrauen. Wir hatten einen Fall, in dem der Server auf ein echtes Zertifikat umgestellt wurde und die Verbindung unterbrochen wurde, aber es wäre in Ordnung gewesen, wenn wir dem Standard-Keystore vertraut hätten. 13 feb. 142014-02-13 19:25:02

  0

@erickson Die API ist ein wenig irreführend: Sie impliziert, dass Sie ein Array von TrustManagers an SSLContext.init() übergeben können, aber das Javadoc für diese Methode gibt an, dass nur der erste TrustManager im Array verwendet wird. Ich würde gerne irgendwelche Einsichten hören, die du teilen kannst. 13 feb. 142014-02-13 19:26:18

  0

@skiphoppy Ich werde ein bisschen damit experimentieren müssen und zu dir zurückkommen. Ich habe es vorher noch nicht gemacht, aber ich habe eine Idee, die funktionieren könnte. 13 feb. 142014-02-13 20:03:55

+1

@skiphoppy Hmm, der einzige Weg, den ich gefunden habe, ist einen temporären 'KeyStore' (im Speicher) zu erstellen und root certs aus all deinen" echten "Keystores hinzuzufügen, oder (fast äquivalent) einen' Set <TrustAnchor>' zu erstellen aus allen Einträgen in Ihren vertrauenswürdigen Schlüsselspeichern und verwenden Sie diese, um "PKIXParameters" und damit "TrustManagerFactory" zu initialisieren. 13 feb. 142014-02-13 21:48:48

  0

Danke @erickson Ich spiele immer noch damit und wandere durch ein langes Labyrinth von Dokumentationen. Ich werde das versuchen. 13 feb. 142014-02-13 22:03:09


12

Wir kopieren den Truststore der JRE und fügen unsere benutzerdefinierten Zertifikate zu diesem Truststore hinzu. Weisen Sie dann die Anwendung an, den benutzerdefinierten Truststore mit einer Systemeigenschaft zu verwenden. Auf diese Weise verlassen wir den standardmäßigen JRE-Truststore.

Der Nachteil ist, dass Sie beim Aktualisieren der JRE den neuen Truststore nicht automatisch mit Ihrem benutzerdefinierten Truststore zusammenführen.

Sie könnten dieses Szenario möglicherweise behandeln, indem Sie ein Installationsprogramm oder eine Startroutine verwenden, die den Truststore/jdk überprüft und nach einem Konflikt sucht oder den Truststore automatisch aktualisiert. Ich weiß nicht, was passiert, wenn Sie den Truststore aktualisieren, während die Anwendung läuft.

Diese Lösung ist nicht 100% elegant oder narrensicher, aber sie ist einfach, funktioniert und erfordert keinen Code.


12

Ich habe so etwas wie dies zu tun ist, wenn commons-Httpclient für den Zugriff auf einen internen HTTPS-Server mit einem selbstsignierten Zertifikat. Ja, unsere Lösung bestand darin, einen benutzerdefinierten TrustManager zu erstellen, der einfach alles weiterleitete (Protokollierung einer Debug-Nachricht).

Das kommt daher, dass wir unsere eigene SSLSocketFactory haben, die SSL-Sockets von unserem lokalen SSLContext erzeugt, der so eingerichtet ist, dass nur unser lokaler TrustManager damit verbunden ist. Sie müssen nicht in der Nähe eines Keystores/Certstores gehen.

So ist dies in unserem 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); 
} 

Zusammen mit anderen Methoden der Umsetzung SecureProtocolSocketFactory. LocalSSLTrustManager ist die zuvor erwähnte Dummy-Trust-Manager-Implementierung.

+7

Wenn Sie die Vertrauensverifizierung deaktivieren, ist die Verwendung von SSL/TLS an erster Stelle wenig sinnvoll. Es ist in Ordnung, lokal zu testen, aber nicht, wenn Sie eine Verbindung nach draußen herstellen möchten. 23 dez. 112011-12-23 13:26:31

  0

Ich bekomme diese Ausnahme, wenn sie auf Java 7 ausgeführt wird. Javax.net.ssl.SSLHandshakeException: keine Cipher-Suites gemeinsam Können Sie helfen? 19 dez. 132013-12-19 12:55:10


14

Wenn eine SSLSocketFactory Schaffung keine Option ist, importieren Sie einfach den Schlüssel in die JVM

  1. Abrufen des öffentlichen Schlüssels: $openssl s_client -connect dev-server:443, dann erstellen Sie eine Datei dev-server.pem, die wie

    aussieht
    -----BEGIN CERTIFICATE----- 
    lklkkkllklklklklllkllklkl 
    lklkkkllklklklklllkllklkl 
    lklkkkllklk.... 
    -----END CERTIFICATE----- 
    
  2. Importieren Sie den Schlüssel: #keytool -import -alias dev-server -keystore $JAVA_HOME/jre/lib/security/cacerts -file dev-server.pem. Passwort: changeit

  3. Restart JVM

Quelle: How to solve javax.net.ssl.SSLHandshakeException?

+1

Ich glaube nicht, dass dies die ursprüngliche Frage löst, aber es hat * mein * Problem gelöst, also danke! 15 mai. 132013-05-15 15:10:40


11

ich durch viele Plätze gingen in SO und die Bahn diese Sache zu lösen. Dies ist der Code, der für mich gearbeitet:

   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 ist ein String, der das Zertifikat enthält, zum Beispiel:

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

ich getestet habe, dass Sie alle Zeichen in der Zertifikat-String setzen können , wenn es selbst signiert ist, solange Sie die genaue Struktur oben halten. Ich habe die Zertifikat-Zeichenfolge mit der Terminal-Befehlszeile meines Laptops erhalten.