Предположим, что я хочу, чтобы все соединения через SSL в моем Java-приложении запрашивали разрешение пользователя, когда встречается ненадежный или просроченный сертификат сервера (как это делают большинство веб-браузеров).
Казалось, что самый естественный способ сделать это — подставить контекст SSL по умолчанию при запуске приложения.
Вот минимальный рабочий пример:
public class ClientAuthentication {
public static final String SERVER_URL = "https://my.test.server";
public static void main(String[] args) throws Exception {
SSLContext defaultContext = SSLContext.getInstance("TLS");
defaultContext.init(null, new TrustManager[]{new MyX509TrustManager()}, null);
// defaultContext.init(null, null, null);
SSLContext.setDefault(defaultContext);
HttpURLConnection connection = (HttpURLConnection) new URL(SERVER_URL).openConnection();
System.out.println(connection.getResponseCode());
}
private static class MyX509TrustManager implements X509TrustManager {
@Override
public void checkClientTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException {
throw new UnsupportedOperationException("Won't be called by client");
}
@Override
public void checkServerTrusted(X509Certificate[] chain, String s) throws CertificateException {
if (userAcceptsCertificate(chain)) {
System.out.format("Certificate '%s' accepted%n", chain[0].getIssuerX500Principal().getName());
}
}
@Override
public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[0];
}
private boolean userAcceptsCertificate(X509Certificate[] x509Certificates) {
// ...
return true;
}
}
}
К сожалению, это не работает, когда сервер требует аутентификации клиента. Даже если хранилище ключей, содержащее клиентский сертификат, правильно указано с помощью параметра виртуальной машины -Djavax.net.ssl.keyStore
, соединение завершается ошибкой с javax.net.ssl.SSLHandshakeException: Received fatal alert: handshake_failure
, поскольку клиентский сертификат вообще не был отправлен (отслеживается в Wireshark).
Такое поведение немного удивительно, потому что JavaDoc для SSLContext#init(KeyManager[], TrustManager[], SecureRandom)
указывает, что передача null
вместо менеджеров ключей или менеджеров доверия означает, что будет выполнена процедура поиска по умолчанию.
И наоборот, если я передам null
вместо массива с моим собственным менеджером доверия в качестве второго аргумента для init
и укажу хранилище доверия JKS, содержащее сертификат сервера, используя-Djavax.net.ssl.trustStore
параметры виртуальной машины, сертификат сервера будет найден там и успешно принят< /эм>.
Итак, можете ли вы посоветовать способ переопределить контекст SSL по умолчанию, заменив только менеджера доверия и оставив поведение по умолчанию в отношении поиска менеджеров ключей? Или может быть какой-то надежный способ найти диспетчер ключей, указанный в параметрах виртуальной машины.
Заметки
Если я вообще не настраиваю контекст SSL по умолчанию, соединение установлено успешно, т. е. все пути, пароли и типы хранилищ верны в параметрах виртуальной машины.
Я никак не могу вызвать
SSLContext.getDefault()
илиSSLContext.getInstance("Default")
, потому что контекст по умолчанию может быть инициализирован только один раз.