Принять самоподписанный 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?

K.Nicholas
источник
Вопрос о самозаверяющем сертификате. Этот ответ не
Маркиз Лорн
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 (как в рабочей среде), даже если он не работает на производственном оборудовании.

Spiffy
источник
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

VSK
источник
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 или заменить его этим файлом.
user66660
источник
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.

Дэвид Райан
источник