Jak mogę używać różnych certyfikatów w określonych połączeniach?

164

Moduł, który dodam do naszej dużej aplikacji Java, musi komunikować się z witryną innej firmy zabezpieczoną protokołem SSL. Problem polega na tym, że witryna używa certyfikatu z podpisem własnym. Mam kopię certyfikatu, aby sprawdzić, czy nie mam ataku typu man-in-the-middle i muszę włączyć ten certyfikat do naszego kodu w taki sposób, aby połączenie z serwerem zakończyło się sukcesem.

Oto podstawowy kod:

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

Bez żadnej dodatkowej obsługi certyfikatu z podpisem własnym umiera to w conn.getOutputStream () z następującym wyjątkiem:

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

Idealnie, mój kod musi nauczyć Javę akceptować ten jeden certyfikat z podpisem własnym dla tego jednego miejsca w aplikacji i nigdzie indziej.

Wiem, że mogę zaimportować certyfikat do magazynu urzędu certyfikacji JRE, a to pozwoli Javie go zaakceptować. To nie jest podejście, które chcę przyjąć, jeśli mogę pomóc; wydaje się to bardzo inwazyjne na wszystkich maszynach naszych klientów dla jednego modułu, którego mogą nie używać; wpłynęłoby to na wszystkie inne aplikacje Java korzystające z tego samego środowiska JRE i nie podoba mi się to, mimo że prawdopodobieństwo uzyskania dostępu do tej witryny przez jakąkolwiek inną aplikację Java jest zerowe. Nie jest to również prosta operacja: na UNIX-ie muszę uzyskać prawa dostępu, aby zmodyfikować środowisko JRE w ten sposób.

Widziałem również, że mogę utworzyć instancję TrustManager, która wykonuje niestandardowe sprawdzanie. Wygląda na to, że mógłbym nawet stworzyć TrustManager, który deleguje do prawdziwego TrustManagera we wszystkich przypadkach z wyjątkiem tego jednego certyfikatu. Ale wygląda na to, że TrustManager jest instalowany globalnie i przypuszczam, że wpłynie to na wszystkie inne połączenia z naszej aplikacji, i to też nie pachnie dobrze.

Jaki jest preferowany, standardowy lub najlepszy sposób skonfigurowania aplikacji Java tak, aby akceptowała certyfikat z podpisem własnym? Czy mogę osiągnąć wszystkie cele, o których myślę powyżej, czy będę musiał pójść na kompromis? Czy jest opcja obejmująca pliki i katalogi oraz ustawienia konfiguracyjne i mało kodu?

skiphoppy
źródło
mała, działająca poprawka: rgagnon.com/javadetails/ ...
20
@Hasenpriester: proszę nie sugerować tej strony. Wyłącza wszystkie weryfikacje zaufania. Nie tylko zaakceptujesz samopodpisany certyfikat, który chcesz, ale także każdy certyfikat, który przedstawi ci osoba atakująca MITM.
Bruno

Odpowiedzi:

168

Utwórz SSLSocketfabrykę samodzielnie i ustaw ją HttpsURLConnectionprzed podłączeniem.

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

Będziesz chciał go utworzyć SSLSocketFactoryi zachować. Oto szkic, jak go zainicjować:

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

Jeśli potrzebujesz pomocy przy tworzeniu magazynu kluczy, skomentuj.


Oto przykład ładowania magazynu kluczy:

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

Aby utworzyć magazyn kluczy z certyfikatem w formacie PEM, możesz napisać własny kod przy użyciu CertificateFactorylub po prostu zaimportować go z keytoolJDK (narzędzie keytool nie będzie działać dla „wpisu klucza”, ale jest dobre dla „zaufanego wpisu” ).

keytool -import -file selfsigned.pem -alias server -keystore server.jks
erickson
źródło
3
Dziękuję Ci bardzo! Inni faceci pomogli mi wskazać mi właściwy kierunek, ale ostatecznie Twoje jest podejściem, które obrałem. Miałem wyczerpujące zadanie konwertowania pliku certyfikatu PEM do pliku kluczy JKS Java i znalazłem pomoc tutaj: stackoverflow.com/questions/722931/ ...
skiphoppy
1
Cieszę się, że się udało. Przykro mi, że walczyłeś ze sklepem z kluczami; Powinienem był po prostu zawrzeć to w mojej odpowiedzi. Z CertificateFactory nie jest to zbyt trudne. Właściwie myślę, że zrobię aktualizację dla każdego, kto przyjdzie później.
erickson
1
Wszystko, co robi ten kod, to replikacja tego, co można osiągnąć, ustawiając trzy właściwości systemu opisane w podręczniku JSSE Refernence Guide.
Markiz Lorne
2
@EJP: To było jakiś czas temu, więc nie pamiętam na pewno, ale domyślam się, że uzasadnieniem było to, że w „dużej aplikacji Java” prawdopodobnie nawiązywane są inne połączenia HTTP. Ustawienie właściwości globalnych może zakłócać działające połączenia lub pozwolić tej stronie na fałszywe serwery. To jest przykład korzystania z wbudowanego menedżera zaufania w poszczególnych przypadkach.
erickson
2
Jak powiedział OP: „mój kod musi nauczyć Javę akceptować ten jeden certyfikat z podpisem własnym, dla tego jednego miejsca w aplikacji i nigdzie indziej”.
erickson
18

Przeczytałem WIELE miejsc w Internecie, aby rozwiązać ten problem. Oto kod, który napisałem, aby działał:

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 to ciąg zawierający certyfikat, na przykład:

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

Przetestowałem, że możesz umieścić dowolne znaki w ciągu certyfikatu, jeśli jest on samopodpisany, o ile zachowasz dokładną strukturę powyżej. Otrzymałem ciąg certyfikatu z wiersza poleceń terminala mojego laptopa.

Josh
źródło
1
Dzięki za udostępnienie @Josh. Stworzyłem mały projekt Github, który demonstruje twój kod w użyciu: github.com/aasaru/ConnectToTrustedServerExample
Master
14

Jeśli tworzenie SSLSocketFactorynie wchodzi w grę, po prostu zaimportuj klucz do maszyny JVM

  1. Odzyskaj klucz publiczny:, $openssl s_client -connect dev-server:443a następnie utwórz plik dev-server.pem, który wygląda jak

    -----BEGIN CERTIFICATE----- 
    lklkkkllklklklklllkllklkl
    lklkkkllklklklklllkllklkl
    lklkkkllklk....
    -----END CERTIFICATE-----
  2. Zaimportować klucz: #keytool -import -alias dev-server -keystore $JAVA_HOME/jre/lib/security/cacerts -file dev-server.pem. Hasło: changeit

  3. Uruchom ponownie JVM

Źródło: Jak rozwiązać wyjątek javax.net.ssl.SSLHandshakeException?

user454322
źródło
1
Myślę, że to nie rozwiązuje pierwotnego pytania, ale rozwiązało mój problem, więc dzięki!
Ed Norris
Nie jest to również dobry pomysł: teraz masz ten losowy, dodatkowy systemowy certyfikat z podpisem własnym, któremu domyślnie będą ufać wszystkie procesy Java.
user268396
12

Kopiujemy magazyn zaufanych certyfikatów środowiska JRE i dodajemy do niego niestandardowe certyfikaty, a następnie informujemy aplikację, aby używała niestandardowego magazynu zaufanych certyfikatów z właściwością systemową. W ten sposób pozostawiamy w spokoju domyślny magazyn zaufanych certyfikatów JRE.

Wadą jest to, że po zaktualizowaniu środowiska JRE nowy magazyn zaufanych certyfikatów nie jest automatycznie łączony z niestandardowym.

Możesz sobie poradzić z tym scenariuszem, korzystając z instalatora lub procedury startowej, która weryfikuje magazyn zaufanych certyfikatów / jdk i sprawdza, czy nie ma niezgodności lub automatycznie aktualizuje magazyn zaufanych certyfikatów. Nie wiem, co się stanie, jeśli zaktualizujesz truststore podczas działania aplikacji.

To rozwiązanie nie jest w 100% eleganckie ani niezawodne, ale jest proste, działa i nie wymaga kodu.

Pan Lśniący i Nowy 安 宇
źródło
12

Musiałem zrobić coś takiego, używając commons-httpclient, aby uzyskać dostęp do wewnętrznego serwera https z certyfikatem z podpisem własnym. Tak, naszym rozwiązaniem było stworzenie niestandardowego TrustManagera, który po prostu przekazywał wszystko (rejestrował komunikat debugowania).

Sprowadza się to do posiadania własnego SSLSocketFactory, który tworzy gniazda SSL z naszego lokalnego SSLContext, który jest skonfigurowany tak, aby był z nim powiązany tylko nasz lokalny TrustManager. W ogóle nie musisz zbliżać się do keystore / certstore.

Więc to jest w naszej 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);
}

Wraz z innymi metodami implementującymi SecureProtocolSocketFactory. LocalSSLTrustManager to wspomniana wcześniej fałszywa implementacja menedżera zaufania.

araqnid
źródło
8
Jeśli wyłączysz całą weryfikację zaufania, nie ma sensu używać SSL / TLS w pierwszej kolejności. Możesz testować lokalnie, ale nie możesz łączyć się na zewnątrz.
Bruno
Ten wyjątek pojawia się podczas uruchamiania go w Javie 7. javax.net.ssl.SSLHandshakeException: brak wspólnych zestawów szyfrów. Czy możesz pomóc?
Uri Lukach