Разрешить небезопасное соединение HTTPS для Java JDK 11 HttpClient
Иногда необходимо разрешить небезопасные соединения HTTPS, например, в некоторых приложениях для веб-сканирования, которые должны работать с любым сайтом. Я использовал одно такое решение со старым API-интерфейсом HttpsURLConnection, который недавно был заменен новым API-интерфейсом HttpClient в JDK 11. Как разрешить небезопасные соединения HTTPS (самозаверяющий или устаревший сертификат) с этим новым API?
UPD: код, который я пробовал (в Kotlin, но отображается непосредственно на Java):
val trustAllCerts = arrayOf<TrustManager>(object: X509TrustManager {
override fun getAcceptedIssuers(): Array<X509Certificate>? = null
override fun checkClientTrusted(certs: Array<X509Certificate>, authType: String) {}
override fun checkServerTrusted(certs: Array<X509Certificate>, authType: String) {}
})
val sslContext = SSLContext.getInstance("SSL")
sslContext.init(null, trustAllCerts, SecureRandom())
val sslParams = SSLParameters()
// This should prevent host validation
sslParams.endpointIdentificationAlgorithm = ""
httpClient = HttpClient.newBuilder()
.sslContext(sslContext)
.sslParameters(sslParams)
.build()
Но при отправке у меня есть исключение (при попытке на localhost с самозаверяющим сертификатом):
java.io.IOException: No name matching localhost found
Использование IP-адреса вместо localhost дает исключение "Отсутствуют альтернативные имена субъектов".
После некоторой отладки JDK я обнаружил, что sslParams
действительно игнорируются в том месте, где выдается исключение, и используется какой-то локально созданный экземпляр. Дальнейшая отладка показала, что единственным способом повлиять на алгоритм проверки имени хоста является настройка jdk.internal.httpclient.disableHostnameVerification
Системное свойство к истине. И это, кажется, решение. SSLParameters
в приведенном выше коде не имеют никакого эффекта, поэтому эту часть можно отбросить. Возможность настройки только в глобальном масштабе выглядит как серьезный недостаток дизайна в новом HttpClient API.
4 ответа
С Java 11 вы также можете сделать то же самое, что упомянуто в выбранном ответе по ссылке, предоставленной HttpClient
построен как:
HttpClient httpClient = HttpClient.newBuilder()
.connectTimeout(Duration.ofMillis(<timeoutInSeconds> * 1000))
.sslContext(sc) // SSL context 'sc' initialised as earlier
.sslParameters(parameters) // ssl parameters if overriden
.build();
с запросом образца
HttpRequest requestBuilder = HttpRequest.newBuilder()
.uri(URI.create("https://www.example.com/getSomething"))
.GET()
.build();
может быть выполнен как:
httpClient.send(requestBuilder, HttpResponse.BodyHandlers.ofString()); // sends the request
Обновление из комментариев, чтобы отключить проверку имени хоста, в настоящее время можно использовать системное свойство:
-Djdk.internal.httpclient.disableHostnameVerification
который может быть установлен программно следующим образом:
final Properties props = System.getProperties();
props.setProperty("jdk.internal.httpclient.disableHostnameVerification", Boolean.TRUE.toString());
Как уже предлагалось, вам нужен SSLContext, который игнорирует плохие сертификаты. Точный код, который получает SSLContext по одной из ссылок в вопросе, должен работать путем создания нулевого TrustManager, который не смотрит на сертификаты:
private static 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) {
}
}
};
public static void main (String[] args) throws Exception {
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, trustAllCerts, new SecureRandom());
HttpClient client = HttpClient.newBuilder()
.sslContext(sslContext)
.build();
Проблема с вышесказанным заключается в том, что аутентификация сервера полностью отключена для всех сайтов. Если был только один неверный сертификат, вы можете импортировать его в хранилище ключей с помощью:
keytool -importcert -keystore keystorename -storepass pass -alias cert -file certfile
и затем инициализируйте SSLContext, используя InputStream, считывающий хранилище ключей следующим образом:
char[] passphrase = ..
KeyStore ks = KeyStore.getInstance("PKCS12");
ks.load(i, passphrase); // i is an InputStream reading the keystore
KeyManagerFactory kmf = KeyManagerFactory.getInstance("PKIX");
kmf.init(ks, passphrase);
TrustManagerFactory tmf = TrustManagerFactory.getInstance("PKIX");
tmf.init(ks);
sslContext = SSLContext.getInstance("TLS");
sslContext.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null);
Любое из вышеуказанных решений будет работать для самозаверяющего сертификата. Третий вариант - в случае, когда сервер предоставляет действительный, неподписанный сертификат, но для хоста, который не совпадает ни с одним из имен в сертификате, который он предоставляет, тогда системное свойство "jdk.internal.httpclient.disableHostnameVerification" может быть установлено в "true", и это заставит сертификат быть принятым так же, как API HostnameVerifier использовался ранее. Обратите внимание, что в обычных развертываниях не ожидается использование какого-либо из этих механизмов, так как должна быть возможность автоматической проверки сертификата, предоставленного любым правильно настроенным сервером HTTPS.
Предоставления X509TrustManager, который не выполняет проверку сертификата (как в ответах выше), недостаточно для отключения проверки имени хоста, поскольку реализация SSLContext «обертывает» предоставленный X509TrustManager с помощью X509ExtendedTrustManager, если это не X509ExtendedTrustManager. Оболочка «Расширенная» X509ExtendedTrustManager выполняет проверку имени хоста.
Таким образом, способ избежать проверки имени хоста — предоставить X509ExtendedTrustManager, который не выполняет проверку имени хоста. Это можно сделать следующим образом:
SSLContext context = SSLContext.getInstance("TLS");
context.init(
null,
new TrustManager[]
{
new X509ExtendedTrustManager()
{
public X509Certificate[] getAcceptedIssuers()
{
return null;
}
public void checkClientTrusted(
final X509Certificate[] a_certificates,
final String a_auth_type)
{
}
public void checkServerTrusted(
final X509Certificate[] a_certificates,
final String a_auth_type)
{
}
public void checkClientTrusted(
final X509Certificate[] a_certificates,
final String a_auth_type,
final Socket a_socket)
{
}
public void checkServerTrusted(
final X509Certificate[] a_certificates,
final String a_auth_type,
final Socket a_socket)
{
}
public void checkClientTrusted(
final X509Certificate[] a_certificates,
final String a_auth_type,
final SSLEngine a_engine)
{
}
public void checkServerTrusted(
final X509Certificate[] a_certificates,
final String a_auth_type,
final SSLEngine a_engine)
{
}
}
},
null);
Предоставьте этот SSLContext в HTTPClient.Builder обычным способом (как показано в предыдущих ответах).
Преимущество этого метода в том, что он применяется для каждого HttpClient (т. е. не для всей JVM).
Имейте в виду, что все предыдущие ответы делают больше, чем просто отключение проверки имени хоста. Они полностью отключают проверку сертификата.
Хотя эти решения идеально подходят для сред разработки, вы не захотите использовать их в производственной среде.
Вот решение, которое позволяет вам изменить сертификат так, как вы считаете наиболее подходящим (например, заменить SAN на другой).
Сначала класс, который используется для оболочки TrustManager по умолчанию для перехвата вызовов:
public class TrustManagers {
/**
* Create {@link TrustManager} wrapper that disables certificate hostname validation
*
* @param tm trust manager
* @param altNames alternative subject names to allow
* @return requested trustmanager
*/
public static TrustManager disableHostnameValidation(
TrustManager tm, Collection<X509SubjectAltName> altNames) {
if (tm instanceof X509ExtendedTrustManager x509tm) {
return new X509ExtendedTrustManager() {
@Override
public void checkServerTrusted(X509Certificate[] chain, String authType)
throws CertificateException {
x509tm.checkServerTrusted(modifySAN(chain), authType);
}
@Override
public void checkServerTrusted(
X509Certificate[] chain, String authType, Socket socket)
throws CertificateException {
x509tm.checkServerTrusted(modifySAN(chain), authType, socket);
}
@Override
public void checkServerTrusted(
X509Certificate[] chain, String authType, SSLEngine engine)
throws CertificateException {
x509tm.checkServerTrusted(modifySAN(chain), authType, engine);
}
private X509Certificate[] modifySAN(X509Certificate[] chain) {
if (chain.length < 1) {
return chain;
}
var result = new X509Certificate[chain.length];
System.arraycopy(chain, 0, result, 0, chain.length);
result[0] = new FixedSANX509Certificate(chain[0], altNames);
return result;
}
// only delegates below
@Override
public X509Certificate[] getAcceptedIssuers() {
return x509tm.getAcceptedIssuers();
}
@Override
public void checkClientTrusted(X509Certificate[] chain, String authType)
throws CertificateException {
x509tm.checkClientTrusted(chain, authType);
}
@Override
public void checkClientTrusted(
X509Certificate[] chain, String authType, Socket socket)
throws CertificateException {
x509tm.checkClientTrusted(chain, authType, socket);
}
@Override
public void checkClientTrusted(
X509Certificate[] chain, String authType, SSLEngine engine)
throws CertificateException {
x509tm.checkClientTrusted(chain, authType);
}
};
}
return tm;
}
}
Затем класс, который позволяет вам изменять содержимое сертификата. Вероятно, вас заинтересуют сети SAN:
/**
* X509Certificate with subject alternative names (SAN) added to list of SANs in certificate. This
* effectively bypasses hostname validation, but allows other validations
*/
@AllArgsConstructor
public class FixedSANX509Certificate extends X509Certificate {
private final X509Certificate cert;
private final Collection<X509SubjectAltName> altNames;
@Override
public Collection<List<?>> getSubjectAlternativeNames() throws CertificateParsingException {
return Stream.concat(
cert.getSubjectAlternativeNames().stream(),
altNames.stream()
.flatMap(
altName ->
Stream.of(
List.of(
altName.type().getCode(),
altName.value()))))
.toList();
}
// only delegates below
@Override
public boolean hasUnsupportedCriticalExtension() {
return cert.hasUnsupportedCriticalExtension();
}
@Override
public Set<String> getCriticalExtensionOIDs() {
return cert.getCriticalExtensionOIDs();
}
@Override
public boolean equals(Object other) {
return cert.equals(other);
}
@Override
public Set<String> getNonCriticalExtensionOIDs() {
return cert.getNonCriticalExtensionOIDs();
}
@Override
public int hashCode() {
return cert.hashCode();
}
@Override
public void checkValidity()
throws CertificateExpiredException, CertificateNotYetValidException {
cert.checkValidity();
}
@Override
public byte[] getEncoded() throws CertificateEncodingException {
return cert.getEncoded();
}
@Override
public void verify(PublicKey key)
throws CertificateException,
NoSuchAlgorithmException,
InvalidKeyException,
NoSuchProviderException,
SignatureException {
cert.verify(key);
}
@Override
public void checkValidity(Date date)
throws CertificateExpiredException, CertificateNotYetValidException {
cert.checkValidity(date);
}
@Override
public byte[] getExtensionValue(String oid) {
return cert.getExtensionValue(oid);
}
@Override
public void verify(PublicKey key, String sigProvider)
throws CertificateException,
NoSuchAlgorithmException,
InvalidKeyException,
NoSuchProviderException,
SignatureException {
cert.verify(key, sigProvider);
}
@Override
public int getVersion() {
return cert.getVersion();
}
@Override
public BigInteger getSerialNumber() {
return cert.getSerialNumber();
}
@Override
public Principal getIssuerDN() {
return cert.getIssuerDN();
}
@Override
public String toString() {
return cert.toString();
}
@Override
public PublicKey getPublicKey() {
return cert.getPublicKey();
}
@Override
public X500Principal getIssuerX500Principal() {
return cert.getIssuerX500Principal();
}
@Override
public Principal getSubjectDN() {
return cert.getSubjectDN();
}
@Override
public X500Principal getSubjectX500Principal() {
return cert.getSubjectX500Principal();
}
@Override
public Date getNotBefore() {
return cert.getNotBefore();
}
@Override
public Date getNotAfter() {
return cert.getNotAfter();
}
@Override
public byte[] getTBSCertificate() throws CertificateEncodingException {
return cert.getTBSCertificate();
}
@Override
public byte[] getSignature() {
return cert.getSignature();
}
@Override
public String getSigAlgName() {
return cert.getSigAlgName();
}
@Override
public String getSigAlgOID() {
return cert.getSigAlgOID();
}
@Override
public byte[] getSigAlgParams() {
return cert.getSigAlgParams();
}
@Override
public boolean[] getIssuerUniqueID() {
return cert.getIssuerUniqueID();
}
@Override
public boolean[] getSubjectUniqueID() {
return cert.getSubjectUniqueID();
}
@Override
public boolean[] getKeyUsage() {
return cert.getKeyUsage();
}
@Override
public List<String> getExtendedKeyUsage() throws CertificateParsingException {
return cert.getExtendedKeyUsage();
}
@Override
public int getBasicConstraints() {
return cert.getBasicConstraints();
}
@Override
public Collection<List<?>> getIssuerAlternativeNames() throws CertificateParsingException {
return cert.getIssuerAlternativeNames();
}
@Override
public void verify(PublicKey key, Provider sigProvider)
throws CertificateException,
NoSuchAlgorithmException,
InvalidKeyException,
SignatureException {
cert.verify(key, sigProvider);
}
}
Классы записи и перечисления, чтобы сделать настройку менее болезненной:
public record X509SubjectAltName(X509SubjectAltType type, String value) {}
/** SAN value types. Aligns with {@link X509Certificate#getSubjectAlternativeNames} */
public enum X509SubjectAltType {
HOSTNAME(2),
IP(7);
final int code;
private X509SubjectAltType(int code) {
this.code = code;
}
public int getCode() {
return code;
}
}
Это будет использоваться для создания SSLContext, например:
private SSLContext createSSLContext(Collection<X509SubjectAltName> altNames)
throws GeneralSecurityException {
var trustManagerFactory =
TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
trustManagerFactory.init((KeyStore) null);
var trustManagers =
Stream.of(trustManagerFactory.getTrustManagers())
.map(tm -> TrustManagers.disableHostnameValidation(tm, altNames))
.toArray(TrustManager[]::new);
var sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, trustManagers, null);
return sslContext;
}
А вот как вы разрешите подключения к ip 1.2.3.4:
createSSLContext(List.of(new X509SubjectAltName(X509SubjectAltType.IP, "1.2.3.4")))