Java и HTTPS-соединение без загрузки сертификата
Описание проблемы
Я пишу код на Java, который подключается к HTTPS сайту, и, судя по всему, не проверяет сертификат. У меня возник вопрос: почему мне не нужно устанавливать сертификат локально для данного сайта? Разве я не должен был установить сертификат и загрузить его для этой программы, или же он загружается автоматически за кулисами? И остается ли трафик между клиентом и удалённым сайтом всё ещё зашифрованным во время передачи?
Ниже приведён код, который я использую:
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.Reader;
import java.net.URL;
import java.net.URLConnection;
import java.security.cert.X509Certificate;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSession;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
public class TestSSL {
public static void main(String[] args) throws Exception {
// Создаем менеджер доверия, который не проверяет цепочки сертификатов
TrustManager[] trustAllCerts = new TrustManager[] { new X509TrustManager() {
public java.security.cert.X509Certificate[] getAcceptedIssuers() {
return null;
}
public void checkClientTrusted(X509Certificate[] certs, String authType) {
}
public void checkServerTrusted(X509Certificate[] certs, String authType) {
}
} };
// Устанавливаем менеджер доверия
final SSLContext sc = SSLContext.getInstance("SSL");
sc.init(null, trustAllCerts, new java.security.SecureRandom());
HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());
// Создаем проверщик имени хоста, который принимает все имена хостов
HostnameVerifier allHostsValid = new HostnameVerifier() {
public boolean verify(String hostname, SSLSession session) {
return true;
}
};
// Устанавливаем проверщик имени хоста
HttpsURLConnection.setDefaultHostnameVerifier(allHostsValid);
URL url = new URL("https://www.google.com");
URLConnection con = url.openConnection();
final Reader reader = new InputStreamReader(con.getInputStream());
final BufferedReader br = new BufferedReader(reader);
String line = "";
while ((line = br.readLine()) != null) {
System.out.println(line);
}
br.close();
} // Конец метода main
} // Конец класса
Буду признателен за разъяснения!
3 ответ(ов)
Почему мне не нужно устанавливать сертификат локально для сайта?
Код, который вы используете, специально предназначен для того, чтобы принимать сертификат без каких-либо проверок. Это не самая лучшая практика, но если это то, что вы хотите сделать, тогда (очевидно) нет необходимости устанавливать сертификат, который ваш код игнорирует.
Неужели мне не нужно устанавливать сертификат локально и загружать его для этой программы, или он скачивается в фоновом режиме?
Нет, и вновь - нет. Смотрите предыдущий ответ.
Шифруется ли трафик между клиентом и удалённым сайтом во время передачи?
Да, он шифруется. Однако проблема заключается в том, что, поскольку вы указали доверять сертификату сервера без проверок, вы не можете быть уверены, что общаетесь с реальным сервером, а не с другим сайтом, который притворяется настоящим сервером. То, является ли это проблемой, зависит от обстоятельств.
Если взять браузер в качестве примера, то обычно браузер не требует от пользователя явной установки сертификата для каждого посещаемого SSL-сайта.
Браузер имеет набор предустановленных доверенных корневых сертификатов. В большинстве случаев, когда вы посещаете сайт с «https», браузер может проверить, что сертификат сайта (в конечном итоге, через цепочку сертификатов) защищён одним из этих доверенных сертификатов. Если браузер не распознает сертификат в начале цепочки как доверенный (или если сертификаты устарели или каким-либо образом недействительны/неуместны), он выдаст предупреждение.
Java работает аналогично. Хранилище ключей JVM имеет набор доверенных сертификатов, и тот же процесс используется для проверки, что сертификат защищён доверенным сертификатом.
Поддерживает ли Java HTTPS клиентский API какой-либо механизм для автоматической загрузки информации о сертификате?
Нет. Разрешение приложениям загружать сертификаты из случайных мест и устанавливать их (как доверенные) в системное хранилище сертификатов было бы серьезной уязвимостью безопасности.
Если вы действительно хотите избежать скачивания сертификата сервера, воспользуйтесь анонимным протоколом, таким как анонимный Диффи-Хеллман (ADH). При использовании ADH сертификат сервера не передается.
Вы можете выбрать анонимный протокол с помощью метода setEnabledCipherSuites
. Список доступных шифров можно посмотреть с помощью getEnabledCipherSuites
.
Связано с этим: именно поэтому в OpenSSL вам необходимо вызывать SSL_get_peer_certificate
. Вы получите X509_V_OK
при использовании анонимного протокола, и именно так вы можете проверить, использовался ли сертификат в процессе обмена.
Однако, как уже отмечали Бруно и Степед С, избегание проверок или использование анонимного протокола — это плохая идея.
Другим вариантом является использование TLS-PSK или TLS-SRP. Эти протоколы также не требуют сертификатов сервера. (Но, по-моему, вы не сможете их использовать).
Дело в том, что вам нужно заранее подготовить систему, поскольку TLS-PSK — это предварительно поделенный секрет, а TLS-SRP — это безопасный удаленный пароль. Аутентификация в этом случае взаимная, а не только со стороны сервера.
В данной ситуации взаимная аутентификация обеспечивается тем, что обе стороны знают общий секрет и приходят к одному и тому же премастер-секрету; или одна (или обе) стороны этого не делают, и настройка канала завершается неудачей. Каждая сторона доказывает знание секрета — это и есть "взаимная" часть.
Наконец, TLS-PSK или TLS-SRP не делают глупостей, таких как раскрытие пароля пользователя, как это происходит в веб-приложениях на HTTP (или HTTPS). Именно поэтому я сказал каждая сторона доказывает знание секрета...
Один из простых, но не чисто Java-решений — это вызов команды curl
из Java, что дает вам полный контроль над выполнением запроса. Если вам нужно сделать что-то простое, этот метод позволяет игнорировать ошибки сертификатов в некоторых случаях. Приведенный ниже пример демонстрирует, как выполнить запрос к защищенному серверу с действительным или недействительным сертификатом, передать куки и получить вывод, используя curl
из Java.
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
public class MyTestClass
{
public static void main(String[] args)
{
String url = "https://www.google.com";
String sessionId = "faf419e0-45a5-47b3-96d1-8c62b2a3b558";
// Опции curl:
// -k: игнорировать ошибки сертификатов
// -L: следовать за редиректами
// -s: без вывода (невербозно)
// -H: добавить HTTP-заголовок
String[] command = { "curl", "-k", "-L", "-s", "-H", "Cookie: MYSESSIONCOOKIENAME=" + sessionId + ";", "-H", "Accept:*/*", url };
String output = executeShellCmd(command, "/tmp", true, true);
System.out.println(output);
}
public static String executeShellCmd(String[] command, String workingFolder, boolean wantsOutput, boolean wantsErrors)
{
try
{
ProcessBuilder pb = new ProcessBuilder(command);
File wf = new File(workingFolder);
pb.directory(wf);
Process proc = pb.start();
BufferedReader stdInput = new BufferedReader(new InputStreamReader(proc.getInputStream()));
BufferedReader stdError = new BufferedReader(new InputStreamReader(proc.getErrorStream()));
StringBuilder sb = new StringBuilder();
String newLine = System.getProperty("line.separator");
String s;
// считываем stdout из команды
if (wantsOutput)
{
while ((s = stdInput.readLine()) != null)
{
sb.append(s);
sb.append(newLine);
}
}
// считываем любые ошибки из выполняемой команды
if (wantsErrors)
{
while ((s = stdError.readLine()) != null)
{
sb.append(s);
sb.append(newLine);
}
}
return sb.toString();
}
catch (IOException e)
{
throw new RuntimeException("Произошла проблема:", e);
}
}
}
Этот код создает процесс, который выполняет команду curl
с заданными параметрами, обрабатывает вывод и ошибки, а затем возвращает результат. Обратите внимание, что данный способ не является "чисто" Java решением, но может быть полезен в некоторых ситуациях.
Регистрация нескольких хранилищ ключей в JVM
Что значит 'synchronized'?
Почему нет ConcurrentHashSet, если есть ConcurrentHashMap?
Как объявить массив в одну строку?
Какие проблемы следует учитывать при переопределении equals и hashCode в Java?