Zaakceptuj samopodpisany certyfikat ssl serwera w kliencie Java

215

To wygląda jak standardowe pytanie, ale nigdzie nie mogłem znaleźć jasnych wskazówek.

Mam kod Java próbujący połączyć się z serwerem z prawdopodobnie podpisanym (lub wygasłym) certyfikatem. Kod zgłasza następujący błąd:

[HttpMethodDirector] I/O exception (javax.net.ssl.SSLHandshakeException) caught 
when processing request: sun.security.validator.ValidatorException: PKIX path 
building failed: sun.security.provider.certpath.SunCertPathBuilderException: 
unable to find valid certification path to requested target

Jak rozumiem, muszę użyć keytool i powiedzieć Javie, że zezwalam na to połączenie.

Wszystkie instrukcje rozwiązania tego problemu zakładają, że jestem w pełni biegły w keytool, np

wygeneruj klucz prywatny dla serwera i zaimportuj go do magazynu kluczy

Czy jest ktoś, kto mógłby opublikować szczegółowe instrukcje?

Używam Uniksa, więc skrypt bash byłby najlepszy.

Nie jestem pewien, czy to ważne, ale kod wykonywany w jboss.

Nikita Rybak
źródło
2
Zobacz Jak zaakceptować samopodpisany certyfikat z Java HttpsURLConnection? . Oczywiście byłoby lepiej, gdybyś zmusił witrynę do korzystania z ważnego certyfikatu.
Matthew Flaschen
2
Dzięki za link, nie widziałem go podczas wyszukiwania. Ale oba rozwiązania wymagają specjalnego kodu do wysłania żądania, a ja używam istniejącego kodu (klient Amazon Amazon dla java). Odpowiednio łączę się z ich witryną i nie mogę naprawić problemów z certyfikatami.
Nikita Rybak
2
@MatthewFlaschen - „Oczywiście lepiej byłoby, gdyby witryna mogła używać ważnego certyfikatu ...” - Certyfikat z podpisem własnym jest ważnym certyfikatem, jeśli klient mu ufa. Wielu uważa, że ​​powierzenie zaufania kartelowi CA / Browser jest wadą bezpieczeństwa.
jww
3
Powiązane, zobacz Najbardziej niebezpieczny kod na świecie: sprawdzanie poprawności certyfikatów SSL w oprogramowaniu innym niż przeglądarka . (Link jest dostępny, ponieważ wydaje się, że otrzymujesz spamerskie odpowiedzi, które wyłączają sprawdzanie poprawności).
jww

Odpowiedzi:

306

Masz tutaj zasadniczo dwie opcje: dodaj samopodpisany certyfikat do swojego magazynu zaufanych certyfikatów JVM lub skonfiguruj do tego klienta

opcja 1

Wyeksportuj certyfikat z przeglądarki i zaimportuj go do swojego magazynu zaufania JVM (w celu ustanowienia łańcucha zaufania):

<JAVA_HOME>\bin\keytool -import -v -trustcacerts
-alias server-alias -file server.cer
-keystore cacerts.jks -keypass changeit
-storepass changeit 

Opcja 2

Wyłącz sprawdzanie poprawności certyfikatu:

// Create a trust manager that does not validate certificate chains
TrustManager[] trustAllCerts = new TrustManager[] { 
    new X509TrustManager() {     
        public java.security.cert.X509Certificate[] getAcceptedIssuers() { 
            return new X509Certificate[0];
        } 
        public void checkClientTrusted( 
            java.security.cert.X509Certificate[] certs, String authType) {
            } 
        public void checkServerTrusted( 
            java.security.cert.X509Certificate[] certs, String authType) {
        }
    } 
}; 

// Install the all-trusting trust manager
try {
    SSLContext sc = SSLContext.getInstance("SSL"); 
    sc.init(null, trustAllCerts, new java.security.SecureRandom()); 
    HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());
} catch (GeneralSecurityException e) {
} 
// Now you can access an https URL without having the certificate in the truststore
try { 
    URL url = new URL("https://hostname/index.html"); 
} catch (MalformedURLException e) {
} 

Pamiętaj, że w ogóle nie polecam opcji nr 2 . Wyłączenie menedżera zaufania niweczy niektóre części SSL i naraża Cię na ataki typu man in the middle. Preferuj opcję 1 lub, jeszcze lepiej, niech serwer używa „prawdziwego” certyfikatu podpisanego przez dobrze znany urząd certyfikacji.

Pascal Thivent
źródło
7
Nie tylko atak MIM. Czyni cię podatnym na połączenie z niewłaściwą witryną. To jest całkowicie niepewne. Zobacz RFC 2246. Jestem przeciwny publikowaniu tego TrustManagera przez cały czas. Nie jest nawet poprawna w stosunku do własnej specyfikacji.
Markiz Lorne
10
@EJP Naprawdę nie polecam drugiej opcji (zaktualizowałem swoją odpowiedź, aby była jasna). Jednak nie opublikowanie go nie rozwiąże niczego (jest to informacja publiczna) i IMHO nie zasługuje na głosowanie negatywne.
Pascal Thivent
135
@EJP To przez uczenie ludzi, że ich edukujesz, a nie przez ukrywanie rzeczy. Więc trzymanie rzeczy w tajemnicy lub w ukryciu nie jest wcale rozwiązaniem. Ten kod jest publiczny, Java API jest publiczny, lepiej o nim rozmawiać, niż go zignorować. Ale mogę żyć z tobą, nie zgadzając się.
Pascal Thivent,
10
Inną opcją - tę, o której nie wspominasz - jest poprawienie certyfikatu serwera przez naprawienie go samodzielnie lub przez skontaktowanie się z odpowiednimi pracownikami pomocy technicznej. Certyfikaty z jednym hostem są naprawdę bardzo tanie; błąkanie się z własnoręcznie podpisanymi rzeczami jest głupie jak funta głupka (tj. dla tych, którzy nie znają tego angielskiego idiomu, całkowicie głupi zestaw priorytetów, który kosztuje dużo, aby prawie nic nie oszczędzić).
Donal Fellows
2
@Rich, 6 lat spóźnienia, ale możesz także zdobyć certyfikat serwera w Firefoksie, klikając ikonę kłódki -> więcej informacji -> Wyświetl certyfikat -> karta Szczegóły -> Eksportuj ... Jest dobrze ukryty.
Siddhartha,
9

Ścigałem ten problem do dostawcy certyfikatu, który nie jest częścią domyślnych zaufanych hostów JVM JDK 8u74. Dostawcą jest www.identrust.com , ale to nie była domena, z którą próbowałem się połączyć. Ta domena otrzymała certyfikat od tego dostawcy. Zobacz Czy cross-root pokrywa zaufanie domyślną listą w JDK / JRE? - przeczytaj kilka wpisów. Zobacz także Które przeglądarki i systemy operacyjne obsługują Let's Encrypt .

Aby połączyć się z domeną, która mnie zainteresowała, na której wydano certyfikat identrust.com, wykonałem następujące kroki. Zasadniczo musiałem uzyskać identrust.com (DST Root CA X3 certyfikat ), aby zaufać JVM. Byłem w stanie to zrobić za pomocą Apache HttpComponents 4.5 w następujący sposób:

1: Uzyskaj certyfikat od indettrust w Instrukcji pobierania łańcucha certyfikatów . Kliknij link DST Root CA X3 .

2: Zapisz ciąg do pliku o nazwie „DST Root CA X3.pem”. Należy dodać wiersze „----- ROZPOCZNIJ CERTYFIKAT -----” i „----- KONIEC CERTYFIKAT -----” w pliku na początku i na końcu.

3: Utwórz plik kluczy Java, cacerts.jks, za pomocą następującego polecenia:

keytool -import -v -trustcacerts -alias IdenTrust -keypass yourpassword -file dst_root_ca_x3.pem -keystore cacerts.jks -storepass yourpassword

4: Skopiuj wynikowy plik kluczy cacerts.jks do katalogu zasobów aplikacji java / (maven).

5: Użyj poniższego kodu, aby załadować ten plik i dołączyć go do klienta Apache 4.5 HttpClient. To rozwiąże problem dla wszystkich domen, dla których certyfikaty zostały wydane przez indetrust.comutil oracle, zawiera certyfikat w domyślnym magazynie kluczy JRE.

SSLContext sslcontext = SSLContexts.custom()
        .loadTrustMaterial(new File(CalRestClient.class.getResource("/cacerts.jks").getFile()), "yourpasword".toCharArray(),
                new TrustSelfSignedStrategy())
        .build();
// Allow TLSv1 protocol only
SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(
        sslcontext,
        new String[] { "TLSv1" },
        null,
        SSLConnectionSocketFactory.getDefaultHostnameVerifier());
CloseableHttpClient httpclient = HttpClients.custom()
        .setSSLSocketFactory(sslsf)
        .build();

Gdy projekt zostanie skompilowany, plik cacerts.jks zostanie skopiowany do ścieżki klas i załadowany stamtąd. W tym momencie nie testowałem na innych stronach ssl, ale jeśli powyższy kod „łańcuchuje” w tym certyfikacie, to one również będą działać, ale znowu nie wiem.

Odwołanie: niestandardowy kontekst SSL i jak zaakceptować samopodpisany certyfikat za pomocą Java HttpsURLConnection?

K.Nicholas
źródło
Pytanie dotyczy certyfikatu z podpisem własnym. Ta odpowiedź nie jest.
Markiz Lorne
4
Przeczytaj pytanie (i odpowiedz) nieco bliżej.
K.Nicholas,
9

Apache HttpClient 4.5 obsługuje akceptowanie samopodpisanych certyfikatów:

SSLContext sslContext = SSLContexts.custom()
    .loadTrustMaterial(new TrustSelfSignedStrategy())
    .build();
SSLConnectionSocketFactory socketFactory =
    new SSLConnectionSocketFactory(sslContext);
Registry<ConnectionSocketFactory> reg =
    RegistryBuilder.<ConnectionSocketFactory>create()
    .register("https", socketFactory)
    .build();
HttpClientConnectionManager cm = new PoolingHttpClientConnectionManager(reg);        
CloseableHttpClient httpClient = HttpClients.custom()
    .setConnectionManager(cm)
    .build();
HttpGet httpGet = new HttpGet(url);
CloseableHttpResponse sslResponse = httpClient.execute(httpGet);

To buduje fabrykę gniazd SSL, która będzie używać TrustSelfSignedStrategy, rejestruje ją w niestandardowym menedżerze połączeń, a następnie wykonuje HTTP GET za pomocą tego menedżera połączeń.

Zgadzam się z tymi, którzy skandują „nie rób tego podczas produkcji”, jednak istnieją przypadki użycia do akceptowania samopodpisanych certyfikatów poza produkcją; używamy ich w zautomatyzowanych testach integracyjnych, dlatego używamy SSL (podobnie jak w produkcji), nawet jeśli nie jest uruchomiony na sprzęcie produkcyjnym.

fajny
źródło
6

Zamiast ustawiać domyślną fabrykę gniazd (która IMO jest złą rzeczą) - wpłynie to na bieżące połączenie, a nie na każde połączenie SSL, które próbujesz otworzyć:

URLConnection connection = url.openConnection();
    // JMD - this is a better way to do it that doesn't override the default SSL factory.
    if (connection instanceof HttpsURLConnection)
    {
        HttpsURLConnection conHttps = (HttpsURLConnection) connection;
        // Set up a Trust all manager
        TrustManager[] trustAllCerts = new TrustManager[] { new X509TrustManager()
        {

            public java.security.cert.X509Certificate[] getAcceptedIssuers()
            {
                return null;
            }

            public void checkClientTrusted(
                java.security.cert.X509Certificate[] certs, String authType)
            {
            }

            public void checkServerTrusted(
                java.security.cert.X509Certificate[] certs, String authType)
            {
            }
        } };

        // Get a new SSL context
        SSLContext sc = SSLContext.getInstance("TLSv1.2");
        sc.init(null, trustAllCerts, new java.security.SecureRandom());
        // Set our connection to use this SSL context, with the "Trust all" manager in place.
        conHttps.setSSLSocketFactory(sc.getSocketFactory());
        // Also force it to trust all hosts
        HostnameVerifier allHostsValid = new HostnameVerifier() {
            public boolean verify(String hostname, SSLSession session) {
                return true;
            }
        };
        // and set the hostname verifier.
        conHttps.setHostnameVerifier(allHostsValid);
    }
InputStream stream = connection.getInputStream();
Jon Daniel
źródło
2
Użytkownik checkServerTrustednie implementuje logiki niezbędnej do faktycznego zaufania do certyfikatu i zapewnienia, że ​​niezaufane certyfikaty zostaną odrzucone. Może to być dobry odczyt: Najbardziej niebezpieczny kod na świecie: sprawdzanie poprawności certyfikatów SSL w oprogramowaniu innym niż przeglądarka .
jww
1
Twoja getAcceptedIssuers()metoda nie jest zgodna ze specyfikacją, a to „rozwiązanie” pozostaje całkowicie niepewne.
Markiz Lorne
4

Istnieje lepsza alternatywa dla ufania wszystkim certyfikatom: Utwórz certyfikat, TrustStorektóry ufa konkretnemu certyfikatowi, i użyj go, aby utworzyć, SSLContextz którego można uzyskać SSLSocketFactoryustawienie na HttpsURLConnection. Oto pełny kod:

File crtFile = new File("server.crt");
Certificate certificate = CertificateFactory.getInstance("X.509").generateCertificate(new FileInputStream(crtFile));

KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
keyStore.load(null, null);
keyStore.setCertificateEntry("server", certificate);

TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
trustManagerFactory.init(keyStore);

SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, trustManagerFactory.getTrustManagers(), null);

HttpsURLConnection connection = (HttpsURLConnection) new URL(url).openConnection();
connection.setSSLSocketFactory(sslContext.getSocketFactory());

Alternatywnie można załadować KeyStorebezpośrednio z pliku lub pobrać certyfikat X.509 z dowolnego zaufanego źródła.

Pamiętaj, że przy tym kodzie certyfikaty cacertsnie będą używane. Ten konkretny HttpsURLConnectionzaufa tylko temu konkretnemu certyfikatowi.

Johannes Brodwall
źródło
1

Ufaj wszystkim certyfikatom SSL: - Możesz ominąć SSL, jeśli chcesz przetestować na serwerze testowym. Ale nie używaj tego kodu do produkcji.

public static class NukeSSLCerts {
protected static final String TAG = "NukeSSLCerts";

public static void nuke() {
    try {
        TrustManager[] trustAllCerts = new TrustManager[] { 
            new X509TrustManager() {
                public X509Certificate[] getAcceptedIssuers() {
                    X509Certificate[] myTrustedAnchors = new X509Certificate[0];  
                    return myTrustedAnchors;
                }

                @Override
                public void checkClientTrusted(X509Certificate[] certs, String authType) {}

                @Override
                public void checkServerTrusted(X509Certificate[] certs, String authType) {}
            }
        };

        SSLContext sc = SSLContext.getInstance("SSL");
        sc.init(null, trustAllCerts, new SecureRandom());
        HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());
        HttpsURLConnection.setDefaultHostnameVerifier(new HostnameVerifier() {
            @Override
            public boolean verify(String arg0, SSLSession arg1) {
                return true;
            }
        });
    } catch (Exception e) { 
    }
}

}

Wywołaj tę funkcję w funkcji onCreate () w działaniu lub w swojej klasie aplikacji.

NukeSSLCerts.nuke();

Można tego użyć do Volley w Androidzie.

Ashish Saini
źródło
Nie jestem do końca pewny, dlaczego przegłosowano, chyba że kod nie działa. Być może jest to założenie, że Android jest w jakiś sposób zaangażowany, gdy pierwotne pytanie nie oznaczało Androida.
Lo-Tan,
1

Przyjęta odpowiedź jest w porządku, ale chciałbym coś do tego dodać, ponieważ korzystałem z IntelliJ na Macu i nie mogłem tego uruchomić przy użyciu JAVA_HOMEzmiennej path.

Okazuje się, że Java Home była inna podczas uruchamiania aplikacji z IntelliJ.

Aby dowiedzieć się dokładnie, gdzie to jest, możesz po prostu zrobić System.getProperty("java.home"), skąd odczytywane są zaufane certyfikaty.

Christopher Schneider
źródło
0

Jeśli „używają” certyfikatu z podpisem własnym, to od nich zależy, czy uczynią ich serwer użytecznym. W szczególności oznacza to przekazanie ich certyfikatu offline w sposób godny zaufania. Niech więc to zrobią. Następnie zaimportuj go do swojego magazynu zaufanych certyfikatów za pomocą narzędzia klucza, zgodnie z opisem w Przewodniku informacyjnym JSSE. Nawet nie myśl o niepewnym opublikowanym tutaj TrustManager.

EDYCJA Z korzyścią dla siedemnastu (!) Downvoterów i licznych komentujących poniżej, którzy najwyraźniej tak naprawdę nie przeczytali tego, co tu napisałem, nie jest to szarpnięcie wobec samopodpisanych certyfikatów. Nie ma nic złego w samopodpisanych certyfikatach, jeśli są one poprawnie wdrożone. Ale poprawnym sposobem na ich wdrożenie jest bezpieczne dostarczenie certyfikatu za pośrednictwem procesu offline, a nie przez nieuwierzytelniony kanał, którego będą używać do uwierzytelnienia. Z pewnością to oczywiste? Jest to z pewnością oczywiste dla każdej organizacji dbającej o bezpieczeństwo, w której kiedykolwiek pracowałem, od banków z tysiącami oddziałów po moje własne firmy. Bazowe na kodzie „rozwiązanie” oparte na kodzie, polegające na zaufaniu wszystkimcertyfikaty, w tym certyfikaty z podpisem własnym, podpisane przez absolutnie nikogo lub jakikolwiek organ arbitrażowy ustanawiający się jako CA, nie są ipso faktycznienie zabezpieczone. Po prostu gra na bezpieczeństwo. To jest bezcelowe. Prowadzisz prywatną, odporną na manipulacje, odporną na odpowiedź i wstrzykiwalną rozmowę z ... kimś. Ktoś. Mężczyzna w środku. Impersonator. Ktoś. Równie dobrze możesz użyć zwykłego tekstu.

Markiz Lorne
źródło
2
To, że jakiś serwer zdecydował się na użycie protokołu https, nie oznacza, że osoba z klientem dba o bezpieczeństwo dla własnych celów.
Gus,
3
Dziwię się, że ta odpowiedź została tak bardzo doceniona. Chciałbym zrozumieć trochę więcej, dlaczego. Wygląda na to, że EJP sugeruje, że nie powinieneś robić opcji 2, ponieważ pozwala to na poważną lukę w zabezpieczeniach. Czy ktoś mógłby wyjaśnić, dlaczego (oprócz ustawień przedwdrożeniowych) dlaczego ta odpowiedź jest niskiej jakości?
cr1pto
2
Cóż, chyba wszystko. Co znaczy „od nich zależy, czy podejmą kroki niezbędne do tego, aby ich serwer był użyteczny” ? Co musi zrobić operator serwera, aby był użyteczny? Jakie kroki masz na myśli? Czy możesz podać ich listę? Ale cofając się do 1000 stóp, jak to się ma do problemu, o który zwrócił się OP? Chce wiedzieć, jak zmusić klienta do zaakceptowania samopodpisanego certyfikatu w Javie. To prosta kwestia powierzenia zaufania do kodu, ponieważ OP uznaje certyfikat za akceptowalny.
jww
2
@EJP - Popraw mnie, jeśli się mylę, ale zaakceptowanie samopodpisanego certyfikatu jest decyzją dotyczącą zasad po stronie klienta. Nie ma to nic wspólnego z operacjami po stronie serwera. Klient musi podjąć decyzję. Parzystości muszą współpracować, aby rozwiązać problem kluczowej dystrybucji, ale nie ma to nic wspólnego z użytecznością serwera.
jww
3
@EJP - Na pewno nie powiedziałem: „Ufaj wszystkim certyfikatom” . Być może coś mi brakuje (lub zbyt wiele w to czytasz) ... OP ma certyfikat i jest dla niego akceptowalny. Chce wiedzieć, jak mu zaufać. Prawdopodobnie dzielę włosy, ale certyfikatu nie trzeba dostarczać offline. Należy zastosować weryfikację pozapasmową, ale to nie to samo, co „należy dostarczyć klientowi offline” . Może nawet jest sklepem produkującym serwer i klienta, więc ma niezbędną wiedzę a priori .
jww
0

Zamiast używać keytool zgodnie z sugestią zamieszczoną w górnym komentarzu, w RHEL możesz używać update-ca-trust zaczynając od nowszych wersji RHEL 6. Musisz mieć certyfikat w formacie pem. Następnie

trust anchor <cert.pem>

Edytuj /etc/pki/ca-trust/source/cert.p11-kit i zmień „kategorię certyfikatu: inny wpis” na „kategorię certyfikatu: organ”. (Lub użyj sed, aby to zrobić w skrypcie.) Następnie zrób

update-ca-trust

Kilka zastrzeżeń:

  • Nie mogłem znaleźć „zaufania” na moim serwerze RHEL 6 i yum nie zaoferował jego instalacji. W końcu użyłem go na serwerze RHEL 7 i skopiowałem plik .p11-kit.
  • Aby to działało, może być konieczne update-ca-trust enable. Spowoduje to zastąpienie / etc / pki / java / cacerts dowiązaniem symbolicznym wskazującym na / etc / pki / ca-trust / extract / java / cacerts. (Więc możesz najpierw wykonać kopię zapasową tego pierwszego).
  • Jeśli twój klient Java używa cacertów przechowywanych w innej lokalizacji, będziesz chciał ręcznie zastąpić go dowiązaniem symbolicznym do / etc / pki / ca-trust / extract / java / cacerts lub zastąpić go tym plikiem.
user66660
źródło
0

Miałem problem, że przekazałem adres URL do biblioteki, która dzwoniła. url.openConnection();Dostosowałem odpowiedź jon-daniela,

public class TrustHostUrlStreamHandler extends URLStreamHandler {

    private static final Logger LOG = LoggerFactory.getLogger(TrustHostUrlStreamHandler.class);

    @Override
    protected URLConnection openConnection(final URL url) throws IOException {

        final URLConnection urlConnection = new URL(url.getProtocol(), url.getHost(), url.getPort(), url.getFile()).openConnection();

        // adapated from
        // /programming/2893819/accept-servers-self-signed-ssl-certificate-in-java-client
        if (urlConnection instanceof HttpsURLConnection) {
            final HttpsURLConnection conHttps = (HttpsURLConnection) urlConnection;

            try {
                // Set up a Trust all manager
                final TrustManager[] trustAllCerts = new TrustManager[] { new X509TrustManager() {

                    @Override
                    public java.security.cert.X509Certificate[] getAcceptedIssuers() {
                        return null;
                    }

                    @Override
                    public void checkClientTrusted(final java.security.cert.X509Certificate[] certs, final String authType) {
                    }

                    @Override
                    public void checkServerTrusted(final java.security.cert.X509Certificate[] certs, final String authType) {
                    }
                } };

                // Get a new SSL context
                final SSLContext sc = SSLContext.getInstance("TLSv1.2");
                sc.init(null, trustAllCerts, new java.security.SecureRandom());
                // Set our connection to use this SSL context, with the "Trust all" manager in place.
                conHttps.setSSLSocketFactory(sc.getSocketFactory());
                // Also force it to trust all hosts
                final HostnameVerifier allHostsValid = new HostnameVerifier() {
                    @Override
                    public boolean verify(final String hostname, final SSLSession session) {
                        return true;
                    }
                };

                // and set the hostname verifier.
                conHttps.setHostnameVerifier(allHostsValid);

            } catch (final NoSuchAlgorithmException e) {
                LOG.warn("Failed to override URLConnection.", e);
            } catch (final KeyManagementException e) {
                LOG.warn("Failed to override URLConnection.", e);
            }

        } else {
            LOG.warn("Failed to override URLConnection. Incorrect type: {}", urlConnection.getClass().getName());
        }

        return urlConnection;
    }

}

Za pomocą tej klasy można utworzyć nowy adres URL za pomocą:

trustedUrl = new URL(new URL(originalUrl), "", new TrustHostUrlStreamHandler());
trustedUrl.openConnection();

Ma to tę zaletę, że jest zlokalizowane i nie zastępuje wartości domyślnej URL.openConnection.

David Ryan
źródło