Принять самоподписанный ssl-сертификат сервера в Java-клиенте


215

Это похоже на стандартный вопрос, но я нигде не смог найти четких указаний.

У меня есть код Java, пытающийся подключиться к серверу с, вероятно, самозаверяющим (или просроченным) сертификатом. Код сообщает о следующей ошибке:

[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

Насколько я понимаю, я должен использовать keytool и сказать Java, что все в порядке, чтобы разрешить это соединение.

Все инструкции по устранению этой проблемы предполагают, что я полностью владею keytool, например:

создать приватный ключ для сервера и импортировать его в хранилище ключей

Есть ли кто-нибудь, кто мог бы опубликовать подробные инструкции?

Я использую Unix, поэтому лучше использовать bash-скрипт.

Не уверен, что это важно, но код выполняется в jboss.


2
См. Как я могу принять самозаверяющий сертификат с Java HttpsURLConnection? , Очевидно, было бы лучше, если бы вы могли заставить сайт использовать действующий сертификат.
Мэтью Флашен

2
Спасибо за ссылку, я не видел ее при поиске. Но оба решения включают специальный код для отправки запроса, и я использую существующий код (amazon ws client для java). Соответственно, я подключаюсь к их сайту и не могу исправить проблемы с сертификатами.
Никита Рыбак

2
@MatthewFlaschen - «Очевидно, было бы лучше, если бы вы могли заставить сайт использовать действующий сертификат ...» - Самозаверяющий сертификат является действительным сертификатом, если клиент доверяет ему. Многие считают, что предоставление доверия картелю CA / Browser является дефектом безопасности.
17

3
См. Также «Самый опасный код в мире»: проверка SSL-сертификатов в не браузерном программном обеспечении . (Ссылка указана, поскольку вы, похоже, получаете те спам-ответы, которые отключают проверку).
jww

Ответы:


307

Здесь у вас есть два основных варианта: добавить самозаверяющий сертификат в хранилище доверенных сертификатов JVM или настроить свой клиент на

Опция 1

Экспортируйте сертификат из вашего браузера и импортируйте его в доверенное хранилище JVM (для создания цепочки доверия):

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

Вариант 2

Отключить проверку сертификата:

// 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) {
} 

Обратите внимание, что я не рекомендую вариант № 2 вообще . Отключение диспетчера доверия разрушает некоторые части SSL и делает вас уязвимым для атак посредников. Предпочитайте вариант № 1 или, что еще лучше, используйте на сервере «настоящий» сертификат, подписанный известным центром сертификации.


7
Не только атака MIM. Это делает вас уязвимым для подключения к неправильному сайту. Это совершенно небезопасно. См. RFC 2246. Я против размещения этого TrustManager в любое время. Это даже не правильно с его собственной спецификацией.
маркиз Лорн

10
@EJP Я действительно не рекомендую второй вариант (я обновил свой ответ, чтобы прояснить его). Тем не менее, отсутствие публикации ничего не решит (это общедоступная информация) и не заслуживает отрицательного ответа.
Паскаль Тивент

135
@EJP Вы учите людей, а не скрываете их. Так что хранить вещи в тайне или в безвестности не является решением вообще. Этот код общедоступен, Java API общедоступен, лучше говорить об этом, чем игнорировать его. Но я могу жить с тобой, не соглашаясь.
Паскаль Тивент

10
Другой вариант - тот, который вы не упомянули - это исправить сертификат сервера, либо исправив его самостоятельно, либо позвонив в службу поддержки. Одиночные хост-сертификаты действительно очень дешевы; возиться с самозаверяющими вещами - глупая глупость (то есть для тех, кто не знаком с этой английской идиомой, совершенно глупый набор приоритетов, который стоит много, чтобы спасти почти ничего).
Донал Феллоуз

2
@Rich, на 6 лет позже, но вы также можете получить сертификат сервера в Firefox, щелкнув значок замка -> дополнительная информация -> Просмотреть сертификат -> вкладка Сведения -> Экспорт ... Он хорошо скрыт.
Сиддхартха

9

Я преследовал эту проблему до поставщика сертификатов, который не является частью доверенных хостов JVM по умолчанию JDK 8u74. Поставщик www.identrust.com , но это не тот домен, к которому я пытался подключиться. Этот домен получил свой сертификат от этого провайдера. См. Будет ли покрытие через корневой каталог доверять списку по умолчанию в JDK / JRE? - прочитайте пару записей. Также посмотрите, какие браузеры и операционные системы поддерживают Let's Encrypt .

Итак, чтобы подключиться к интересующему меня домену, у которого был выдан сертификат, identrust.comя сделал следующие шаги. По сути, мне нужно было получить DST Root CA X3сертификат identrust.com ( ) для доверия со стороны JVM. Я смог сделать это с помощью Apache HttpComponents 4.5 следующим образом:

1: Получите сертификат от недоверенного в Инструкции по загрузке цепочки сертификатов . Нажмите на ссылку DST Root CA X3 .

2. Сохраните строку в файл с именем «DST Root CA X3.pem». Обязательно добавьте в файл строки «----- BEGIN CERTIFICATE -----» и «----- END CERTIFICATE -----» в начале и конце файла.

3. Создайте файл хранилища ключей Java cacerts.jks с помощью следующей команды:

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

4: Скопируйте получившееся хранилище ключей cacerts.jks в каталог ресурсов вашего приложения java / (maven).

5: используйте следующий код, чтобы загрузить этот файл и прикрепить его к Apache 4.5 HttpClient. Это решит проблему для всех доменов, у которых есть сертификаты, выданные indetrust.comутилитой Oracle. Включение сертификата в хранилище ключей 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();

Когда проект будет собран, cacerts.jks будет скопирован в путь к классам и загружен оттуда. В данный момент я не тестировал другие ssl-сайты, но если приведенный выше код «цепочек» в этом сертификате, они тоже будут работать, но опять же, я не знаю.

Ссылка: пользовательский контекст SSL и как принять самозаверяющий сертификат с Java HttpsURLConnection?


Вопрос о самозаверяющем сертификате. Этот ответ не
Маркиз Лорн

4
Прочитайте вопрос (и ответьте) чуть ближе.
К.Николас

9

Apache HttpClient 4.5 поддерживает прием самоподписанных сертификатов:

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

Это создает фабрику сокетов SSL, которая будет использовать TrustSelfSignedStrategy, регистрирует ее с помощью настраиваемого диспетчера соединений, а затем выполняет HTTP GET с использованием этого диспетчера соединений.

Я согласен с теми, кто повторяет «не делай этого в производстве», однако есть варианты использования для принятия самозаверяющих сертификатов вне производства; мы используем их в автоматизированных интеграционных тестах, поэтому мы используем SSL (как в рабочей среде), даже если он не работает на производственном оборудовании.


6

Вместо того, чтобы устанавливать фабрику сокетов по умолчанию (что является IMO - плохая вещь) - это повлияет на текущее соединение, а не на каждое соединение SSL, которое вы пытаетесь открыть:

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

2
Вы checkServerTrustedне реализуете необходимую логику, чтобы фактически доверять сертификату и гарантировать, что ненадежные сертификаты отклонены. Это может быть полезным для чтения: Самый опасный код в мире: проверка SSL-сертификатов в не браузерном программном обеспечении .
jww

1
Ваш getAcceptedIssuers()метод не соответствует спецификации, и это «решение» остается совершенно небезопасным.
Маркиз Лорн

4

Существует лучшая альтернатива доверию всем сертификатам: создайте объект, TrustStoreкоторый определенно доверяет данному сертификату, и используйте его для создания объекта, SSLContextиз которого нужно получить SSLSocketFactoryзначение HttpsURLConnection. Вот полный код:

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

Вы также можете загрузить KeyStoreнепосредственно из файла или получить сертификат X.509 из любого надежного источника.

Обратите внимание, что с этим кодом сертификаты в cacertsне будут использоваться. Этот конкретный HttpsURLConnectionбудет доверять только этот конкретный сертификат.


1

Доверяйте всем SSL-сертификатам: - Вы можете обойти SSL, если хотите провести тестирование на тестовом сервере. Но не используйте этот код для производства.

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) { 
    }
}

}

Пожалуйста, вызовите эту функцию в функции onCreate () в Activity или в вашем классе приложения.

NukeSSLCerts.nuke();

Это может быть использовано для залпа в Android.


Не совсем уверен, почему за вас проголосовали, если код не работает. Возможно, это предположение, что Android каким-то образом участвует, когда исходный вопрос не помечал Android.
Ло-Тан

1

Принятый ответ - хорошо, но я хотел бы добавить кое-что к этому, поскольку я использовал IntelliJ на Mac и не мог заставить его работать с помощью JAVA_HOMEпеременной пути.

Оказывается, Java Home отличался при запуске приложения от IntelliJ.

Чтобы точно определить, где он находится, вы можете просто сделать System.getProperty("java.home")так, чтобы именно из него считывались доверенные сертификаты.


0

Если «они» используют самозаверяющий сертификат, они сами должны предпринять шаги, необходимые для обеспечения работоспособности их сервера. В частности, это означает предоставление вам сертификата в автономном режиме надежным способом. Так что заставь их сделать это. Затем вы импортируете это в ваше хранилище доверенных сертификатов, используя keytool, как описано в Справочном руководстве JSSE. Даже не думайте о небезопасном TrustManager, размещенном здесь.

РЕДАКТИРОВАТЬ В интересах семнадцати (!) Downvoters и многочисленных нижеприведенных комментариев, которые явно не читали то, что я здесь написал, это не является иеремией против самозаверяющих сертификатов. Нет ничего плохого в самозаверяющих сертификатах, если они реализованы правильно. Но правильным способом их реализации является безопасная доставка сертификата через автономный процесс, а не через канал без аутентификации, который они будут использовать для аутентификации. Конечно, это очевидно? Это, безусловно, очевидно для каждой организации, работающей в сфере безопасности, в которой я когда-либо работал, от банков с тысячами отделений до моих собственных компаний. Клиентская кодовая база «решение» доверия всемсертификаты, в том числе самозаверяющие сертификаты, подписанные абсолютно кем-либо, или любым арбитражным органом, который создает себя в качестве CA, является ipso factoне является безопасным. Это просто игра в безопасности. Это бессмысленно. У вас частная беседа, защищенная от взлома, ответа и инъекции, с ... кем-то. Кто-нибудь. Человек посередине. Имитатор Кто-нибудь. Вы также можете просто использовать открытый текст.


2
Тот факт, что какой-то сервер решил использовать https, не означает, что человек, имеющий клиента , не понимает, что такое безопасность для его собственных целей.
Гас

3
Я удивлен, что этот ответ получил столько голосов. Я хотел бы понять немного больше, почему. Похоже, EJP предлагает не использовать вариант 2, потому что он допускает серьезный недостаток безопасности. Может ли кто-нибудь объяснить, почему (кроме настроек перед развертыванием) почему этот ответ низкого качества?
cr1pto

2
Ну, я думаю, все это. Что означает «они должны предпринять шаги, необходимые для того, чтобы сделать их сервер пригодным для использования» ? Что должен сделать оператор сервера, чтобы сделать его пригодным для использования? Какие шаги ты имеешь в виду? Можете ли вы предоставить их список? Но, возвращаясь к 1000 футам, как это связано с проблемой, которую задал ОП? Он хочет знать, как заставить клиента принять самоподписанный сертификат в Java. Это просто вопрос доверия к коду, поскольку OP находит сертификат приемлемым.
17

2
@EJP - поправьте меня, если я ошибаюсь, но принятие самозаверяющего сертификата является решением политики на стороне клиента. Это не имеет ничего общего с операциями на стороне сервера. Клиент должен принять решение. Четности должны работать вместе для решения проблемы распределения ключей, но это никак не связано с тем, чтобы сделать сервер пригодным для использования.
17

3
@EJP - я, конечно, не сказал «доверяй всем сертификатам» . Возможно, я что-то упускаю (или вы слишком много читаете) ... У ОП есть сертификат, и он приемлем для него. Он хочет знать, как доверять этому. Я, вероятно, раскалывать волосы, но сертификат не нужно доставлять в автономном режиме. Следует использовать внеполосную проверку, но это не то же самое, что «должен быть доставлен клиенту в автономном режиме» . Он может быть даже магазином, производящим сервер и клиента, поэтому он обладает необходимыми априорными знаниями.
17

0

Это не решение полной проблемы, но у Oracle есть хорошая подробная документация о том, как использовать этот инструмент. Это объясняет, как

  1. используйте keytool.
  2. генерировать сертификаты / самозаверяющие сертификаты, используя keytool.
  3. импортировать сгенерированные сертификаты для клиентов Java.

https://docs.oracle.com/cd/E54932_01/doc.705/e54936/cssg_create_ssl_cert.htm#CSVSG178


0

Вместо использования keytool, как предлагается в верхнем комментарии, в RHEL вы можете использовать update-ca-trust, начиная с более новых версий RHEL 6. Вам потребуется сертификат в формате pem. затем

trust anchor <cert.pem>

Отредактируйте /etc/pki/ca-trust/source/cert.p11-kit и измените «категория сертификата: другая запись» на «категория сертификата: полномочие». (Или используйте sed, чтобы сделать это в скрипте.) Затем сделайте

update-ca-trust

Пара предостережений:

  • Я не смог найти «доверия» на своем сервере RHEL 6, и yum не предложил установить его. В итоге я использовал его на сервере RHEL 7 и скопировал файл .p11-kit.
  • Чтобы сделать эту работу для вас, вам может понадобиться update-ca-trust enable . Это заменит / etc / pki / java / cacerts символической ссылкой, указывающей на / etc / pki / ca-trust / extract / java / cacerts. (Так что, возможно, вы захотите сделать резервную копию первого.)
  • Если ваш java-клиент использует cacerts, хранящиеся в каком-то другом месте, вам нужно вручную заменить его символической ссылкой на / etc / pki / ca-trust / extract / java / cacerts или заменить его этим файлом.

0

У меня была проблема, что я передавал URL в библиотеку, которая звонила, url.openConnection();я адаптировал ответ jon-daniel,

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

}

Используя этот класс, можно создать новый URL с:

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

Это имеет то преимущество, что оно локализовано и не заменяет значение по умолчанию URL.openConnection.

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.