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

164

Ein Modul, das ich unserer großen Java-Anwendung hinzufüge, muss sich mit der SSL-gesicherten Website eines anderen Unternehmens unterhalten. Das Problem ist, dass die Site ein selbstsigniertes Zertifikat verwendet. Ich habe eine Kopie des Zertifikats, um zu überprüfen, ob ich nicht auf einen Man-in-the-Middle-Angriff stoße, und ich muss dieses Zertifikat so in unseren Code integrieren, 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 für das selbstsignierte Zertifikat stirbt dies bei conn.getOutputStream () mit der 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

Im Idealfall muss mein Code Java beibringen, dieses eine selbstsignierte Zertifikat für diesen einen Punkt in der Anwendung und nirgendwo anders zu akzeptieren.

Ich weiß, dass ich das Zertifikat in den Zertifizierungsstellenspeicher der JRE importieren kann, sodass Java es akzeptieren kann. Das ist kein Ansatz, den ich verfolgen möchte, wenn ich helfen kann. Es scheint sehr invasiv zu sein, auf allen Maschinen unserer Kunden ein Modul zu verwenden, das sie möglicherweise nicht verwenden. Es würde alle anderen Java-Anwendungen betreffen, die dieselbe JRE verwenden, und das gefällt mir 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 ändern.

Ich habe auch gesehen, dass ich eine TrustManager-Instanz erstellen kann, die einige benutzerdefinierte Überprüfungen durchführt. Es sieht so aus, als ob ich möglicherweise sogar einen TrustManager erstellen kann, der in allen Fällen mit Ausnahme dieses einen Zertifikats an den echten TrustManager delegiert. Aber es sieht so aus, als würde TrustManager global installiert, und ich nehme an, dass dies alle anderen Verbindungen aus unserer Anwendung betreffen würde, und das riecht auch für mich nicht ganz richtig.

Was ist die bevorzugte, standardmäßige oder beste Methode, um eine Java-Anwendung für die Annahme eines selbstsignierten Zertifikats einzurichten? Kann ich alle oben genannten Ziele erreichen oder muss ich Kompromisse eingehen? Gibt es eine Option, die Dateien und Verzeichnisse und Konfigurationseinstellungen sowie wenig bis gar keinen Code umfasst?

Skiphoppy
quelle
kleine, funktionierende Lösung: rgagnon.com/javadetails/…
20
@Hasenpriester: Bitte schlagen Sie diese Seite nicht vor. Es deaktiviert alle Vertrauensüberprüfungen. Sie akzeptieren nicht nur das selbstsignierte Zertifikat, das Sie möchten, sondern auch jedes Zertifikat, das Ihnen ein MITM-Angreifer vorlegt.
Bruno

Antworten:

168

Erstellen Sie selbst eine SSLSocketFabrik und stellen Sie sie HttpsURLConnectionvor dem Anschließen auf ein.

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

Sie möchten eine erstellen SSLSocketFactoryund behalten. Hier ist eine Skizze zum Initialisieren:

/* 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();

Wenn Sie Hilfe beim Erstellen des Schlüsselspeichers benötigen, kommentieren Sie diese bitte.


Hier ist ein Beispiel für das Laden des Schlüsselspeichers:

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

Um den Schlüsselspeicher mit einem Zertifikat im PEM-Format zu erstellen, können Sie Ihren eigenen Code mit schreiben CertificateFactoryoder ihn einfach keytoolaus dem JDK importieren (Keytool funktioniert nicht für einen "Schlüsseleintrag", ist aber für einen "vertrauenswürdigen Eintrag" in Ordnung. ).

keytool -import -file selfsigned.pem -alias server -keystore server.jks
erickson
quelle
3
Vielen Dank! Die anderen Jungs haben mir geholfen, in die richtige Richtung zu weisen, aber am Ende ist deins der Ansatz, den ich gewählt habe. Ich hatte eine anstrengende Aufgabe, die PEM-Zertifikatdatei in eine JKS-Java-Keystore-Datei zu konvertieren, und ich fand hier Unterstützung dafür: stackoverflow.com/questions/722931/…
skiphoppy
1
Ich bin froh, dass es geklappt hat. Es tut mir leid, dass Sie mit dem Schlüsselspeicher zu kämpfen hatten. Ich hätte es einfach in meine Antwort aufnehmen sollen. Mit CertificateFactory ist das nicht allzu schwierig. Tatsächlich denke ich, dass ich ein Update für jeden machen werde, der später kommt.
Erickson
1
Dieser Code repliziert lediglich das, was Sie erreichen können, indem Sie drei Systemeigenschaften festlegen, die im JSSE-Referenzhandbuch beschrieben sind.
Marquis von Lorne
2
@EJP: Dies ist eine Weile her, daher erinnere ich mich nicht sicher, aber ich vermute, dass in einer "großen Java-Anwendung" wahrscheinlich andere HTTP-Verbindungen hergestellt wurden. Das Festlegen globaler Eigenschaften kann die Arbeitsverbindungen beeinträchtigen oder es dieser einen Partei ermöglichen, Server zu fälschen. Dies ist ein Beispiel für die Verwendung des integrierten Vertrauensmanagers von Fall zu Fall.
Erickson
2
Wie das OP sagte: "Mein Code muss Java beibringen, dieses eine selbstsignierte Zertifikat für diesen einen Punkt in der Anwendung und nirgendwo anders zu akzeptieren."
Erickson
18

Ich habe VIELE Orte online durchgelesen, um dieses Problem zu lösen. Dies ist der Code, den ich geschrieben habe, damit es funktioniert:

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 eine Zeichenfolge, die das Zertifikat enthält, zum Beispiel:

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

Ich habe getestet, dass Sie beliebige Zeichen in die Zertifikatzeichenfolge einfügen können, wenn diese selbst signiert ist, solange Sie die genaue Struktur oben beibehalten. Ich habe die Zertifikatzeichenfolge über die Terminal-Befehlszeile meines Laptops erhalten.

Josh
quelle
1
Danke, dass du @Josh geteilt hast. Ich habe ein kleines Github-Projekt erstellt, das Ihren verwendeten Code demonstriert: github.com/aasaru/ConnectToTrustedServerExample
Master Drools
14

Wenn das Erstellen eines SSLSocketFactorynicht möglich ist, importieren Sie einfach den Schlüssel in die JVM

  1. Rufen Sie den öffentlichen Schlüssel ab : $openssl s_client -connect dev-server:443, und erstellen Sie dann eine Datei dev-server.pem , die 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. Starten Sie JVM neu

Quelle: Wie löse ich javax.net.ssl.SSLHandshakeException?

user454322
quelle
1
Ich glaube nicht, dass dies die ursprüngliche Frage löst, aber es hat mein Problem gelöst , also danke!
Ed Norris
Es ist auch keine gute Idee, dies zu tun: Jetzt haben Sie dieses zufällige zusätzliche systemweite selbstsignierte Zertifikat, dem alle Java-Prozesse standardmäßig vertrauen.
user268396
12

Wir kopieren den Truststore der JRE, fügen diesem Truststore unsere benutzerdefinierten Zertifikate hinzu und weisen die Anwendung an, den benutzerdefinierten Truststore mit einer Systemeigenschaft zu verwenden. Auf diese Weise lassen wir den Standard-JRE-Truststore in Ruhe.

Der Nachteil ist, dass beim Aktualisieren der JRE der neue Truststore nicht automatisch mit Ihrem benutzerdefinierten zusammengeführt wird.

Sie können dieses Szenario möglicherweise mit einem Installationsprogramm oder einer Startroutine behandeln, die den Truststore / JDK überprüft und auf eine Nichtübereinstimmung überprüft oder den Truststore automatisch aktualisiert. Ich weiß nicht, was passiert, wenn Sie den Truststore aktualisieren, während die Anwendung ausgeführt wird.

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

Mr. Shiny und New 安 宇
quelle
12

Ich musste so etwas tun, wenn ich commons-httpclient verwendete, um auf einen internen https-Server mit einem selbstsignierten Zertifikat zuzugreifen. Ja, unsere Lösung bestand darin, einen benutzerdefinierten TrustManager zu erstellen, der einfach alles übergeben hat (Protokollierung einer Debug-Nachricht).

Dies hängt davon ab, dass wir über eine eigene SSLSocketFactory verfügen, die SSL-Sockets aus unserem lokalen SSLContext erstellt, dem nur unser lokaler TrustManager zugeordnet ist. Sie müssen sich überhaupt nicht in der Nähe eines Keystores / Certstores aufhalten.

Das ist also in unserer 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 zur Implementierung von SecureProtocolSocketFactory. LocalSSLTrustManager ist die oben erwähnte Dummy-Trust-Manager-Implementierung.

araqnid
quelle
8
Wenn Sie die gesamte Vertrauensüberprüfung deaktivieren, ist die Verwendung von SSL / TLS zunächst wenig sinnvoll. Es ist in Ordnung, lokal zu testen, aber nicht, wenn Sie eine Verbindung außerhalb herstellen möchten.
Bruno
Ich erhalte diese Ausnahme, wenn es unter Java 7 ausgeführt wird. Javax.net.ssl.SSLHandshakeException: Keine gemeinsamen Cipher Suites Können Sie helfen?
Uri Lukach