Сертификаты клиента Java через HTTPS/SSL
-
22-08-2019 - |
Вопрос
Я использую Java 6 и пытаюсь создать HttpsURLConnection
против удаленного сервера с использованием сертификата клиента.
Сервер использует самозаверяющий корневой сертификат и требует предоставления сертификата клиента, защищенного паролем.Я добавил корневой сертификат сервера и сертификат клиента в хранилище ключей Java по умолчанию, которое я нашел в /System/Library/Frameworks/JavaVM.framework/Versions/1.6.0/Home/lib/security/cacerts
(ОСХ 10.5).Имя файла хранилища ключей, кажется, предполагает, что сертификат клиента не должен туда помещаться?
В любом случае, добавление корневого сертификата в это хранилище решило печально известную проблему. javax.net.ssl.SSLHandshakeException: sun.security.validator.ValidatorException: PKIX path building failed' problem.
Однако теперь я застрял в том, как использовать клиентский сертификат.Я попробовал два подхода, и ни один из них меня ни к чему не привел.
Прежде всего, попробуйте:
SSLSocketFactory sslsocketfactory = (SSLSocketFactory) SSLSocketFactory.getDefault();
URL url = new URL("https://somehost.dk:3049");
HttpsURLConnection conn = (HttpsURLConnection)url.openConnection();
conn.setSSLSocketFactory(sslsocketfactory);
InputStream inputstream = conn.getInputStream();
// The last line fails, and gives:
// javax.net.ssl.SSLHandshakeException: Received fatal alert: handshake_failure
Я попробовал пропустить класс HttpsURLConnection (не идеально, так как я хочу общаться с сервером по HTTP) и вместо этого сделал следующее:
SSLSocketFactory sslsocketfactory = (SSLSocketFactory) SSLSocketFactory.getDefault();
SSLSocket sslsocket = (SSLSocket) sslsocketfactory.createSocket("somehost.dk", 3049);
InputStream inputstream = sslsocket.getInputStream();
// do anything with the inputstream results in:
// java.net.SocketTimeoutException: Read timed out
Я даже не уверен, что проблема здесь в сертификате клиента.
Решение
Наконец-то решил это ;).Получил сильный намек здесь (Ответ Гэндальфа тоже немного затронул эту тему).Недостающие ссылки были (в основном) первым из приведенных ниже параметров, и в некоторой степени я упустил из виду разницу между хранилищами ключей и хранилищами доверенных сертификатов.
Самозаверяющий сертификат сервера необходимо импортировать в хранилище доверенных сертификатов:
keytool -import -aliasgridserver -filegridserver.crt -storepass $PASS -keystoregridserver.keystore
Эти свойства необходимо установить (либо в командной строке, либо в коде):
-Djavax.net.ssl.keyStoreType=pkcs12
-Djavax.net.ssl.trustStoreType=jks
-Djavax.net.ssl.keyStore=clientcertificate.p12
-Djavax.net.ssl.trustStore=gridserver.keystore
-Djavax.net.debug=ssl # very verbose debug
-Djavax.net.ssl.keyStorePassword=$PASS
-Djavax.net.ssl.trustStorePassword=$PASS
Рабочий пример кода:
SSLSocketFactory sslsocketfactory = (SSLSocketFactory) SSLSocketFactory.getDefault();
URL url = new URL("https://gridserver:3049/cgi-bin/ls.py");
HttpsURLConnection conn = (HttpsURLConnection)url.openConnection();
conn.setSSLSocketFactory(sslsocketfactory);
InputStream inputstream = conn.getInputStream();
InputStreamReader inputstreamreader = new InputStreamReader(inputstream);
BufferedReader bufferedreader = new BufferedReader(inputstreamreader);
String string = null;
while ((string = bufferedreader.readLine()) != null) {
System.out.println("Received " + string);
}
Другие советы
Хотя это и не рекомендуется, вы также можете полностью отключить проверку сертификата SSL:
import javax.net.ssl.*;
import java.security.SecureRandom;
import java.security.cert.X509Certificate;
public class SSLTool {
public static void disableCertificateValidation() {
// Create a trust manager that does not validate certificate chains
TrustManager[] trustAllCerts = new TrustManager[] {
new X509TrustManager() {
public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[0];
}
public void checkClientTrusted(X509Certificate[] certs, String authType) {}
public void checkServerTrusted(X509Certificate[] certs, String authType) {}
}};
// Ignore differences between given hostname and certificate hostname
HostnameVerifier hv = new HostnameVerifier() {
public boolean verify(String hostname, SSLSession session) { return true; }
};
// Install the all-trusting trust manager
try {
SSLContext sc = SSLContext.getInstance("SSL");
sc.init(null, trustAllCerts, new SecureRandom());
HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());
HttpsURLConnection.setDefaultHostnameVerifier(hv);
} catch (Exception e) {}
}
}
Вы установили свойства KeyStore и/или TrustStore System?
java -Djavax.net.ssl.keyStore=pathToKeystore -Djavax.net.ssl.keyStorePassword=123456
или с помощью кода
System.setProperty("javax.net.ssl.keyStore", pathToKeyStore);
То же самое с javax.net.ssl.trustStore
Если вы имеете дело с вызовом веб-службы с использованием платформы Axis, есть гораздо более простой ответ.Если все, что вам нужно, — это чтобы ваш клиент мог вызывать веб-службу SSL и игнорировать ошибки сертификата SSL, просто поместите этот оператор перед вызовом каких-либо веб-служб:
System.setProperty("axis.socketSecureFactory",
"org.apache.axis.components.net.SunFakeTrustSocketFactory");
Применяются обычные заявления об отказе от ответственности о том, что это очень плохо в производственной среде.
Я нашел это в Ось вики.
У меня это сработало при использовании Apache HttpComponents ~ HttpClient 4.x:
KeyStore keyStore = KeyStore.getInstance("PKCS12");
FileInputStream instream = new FileInputStream(new File("client-p12-keystore.p12"));
try {
keyStore.load(instream, "helloworld".toCharArray());
} finally {
instream.close();
}
// Trust own CA and all self-signed certs
SSLContext sslcontext = SSLContexts.custom()
.loadKeyMaterial(keyStore, "helloworld".toCharArray())
//.loadTrustMaterial(trustStore, new TrustSelfSignedStrategy()) //custom trust store
.build();
// Allow TLSv1 protocol only
SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(
sslcontext,
new String[] { "TLSv1" },
null,
SSLConnectionSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER); //TODO
CloseableHttpClient httpclient = HttpClients.custom()
.setHostnameVerifier(SSLConnectionSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER) //TODO
.setSSLSocketFactory(sslsf)
.build();
try {
HttpGet httpget = new HttpGet("https://localhost:8443/secure/index");
System.out.println("executing request" + httpget.getRequestLine());
CloseableHttpResponse response = httpclient.execute(httpget);
try {
HttpEntity entity = response.getEntity();
System.out.println("----------------------------------------");
System.out.println(response.getStatusLine());
if (entity != null) {
System.out.println("Response content length: " + entity.getContentLength());
}
EntityUtils.consume(entity);
} finally {
response.close();
}
} finally {
httpclient.close();
}
Файл P12 содержит сертификат клиента и закрытый ключ клиента, созданные с помощью BouncyCastle:
public static byte[] convertPEMToPKCS12(final String keyFile, final String cerFile,
final String password)
throws IOException, CertificateException, KeyStoreException, NoSuchAlgorithmException,
NoSuchProviderException
{
// Get the private key
FileReader reader = new FileReader(keyFile);
PEMParser pem = new PEMParser(reader);
PEMKeyPair pemKeyPair = ((PEMKeyPair)pem.readObject());
JcaPEMKeyConverter jcaPEMKeyConverter = new JcaPEMKeyConverter().setProvider("BC");
KeyPair keyPair = jcaPEMKeyConverter.getKeyPair(pemKeyPair);
PrivateKey key = keyPair.getPrivate();
pem.close();
reader.close();
// Get the certificate
reader = new FileReader(cerFile);
pem = new PEMParser(reader);
X509CertificateHolder certHolder = (X509CertificateHolder) pem.readObject();
java.security.cert.Certificate x509Certificate =
new JcaX509CertificateConverter().setProvider("BC")
.getCertificate(certHolder);
pem.close();
reader.close();
// Put them into a PKCS12 keystore and write it to a byte[]
ByteArrayOutputStream bos = new ByteArrayOutputStream();
KeyStore ks = KeyStore.getInstance("PKCS12", "BC");
ks.load(null);
ks.setKeyEntry("key-alias", (Key) key, password.toCharArray(),
new java.security.cert.Certificate[]{x509Certificate});
ks.store(bos, password.toCharArray());
bos.close();
return bos.toByteArray();
}
Для этого в моем текущем проекте я использую пакет HTTP-клиента Apache commons, и он отлично работает с SSL и самозаверяющим сертификатом (после его установки в cacerts, как вы упомянули).Пожалуйста, взгляните на это здесь:
Я думаю, что у вас проблема с сертификатом вашего сервера, он не является действительным сертификатом (я думаю, что в данном случае это означает «handshake_failure»):
Импортируйте сертификат сервера в хранилище ключей Trustcacerts на клиентской JRE.Это легко сделать с помощью ключевой инструмент:
keytool
-import
-alias <provide_an_alias>
-file <certificate_file>
-keystore <your_path_to_jre>/lib/security/cacerts
Используя приведенный ниже код
-Djavax.net.ssl.keyStoreType=pkcs12
или
System.setProperty("javax.net.ssl.keyStore", pathToKeyStore);
совершенно не требуется.Также нет необходимости создавать собственную фабрику SSL.
Я также столкнулся с той же проблемой, в моем случае была проблема, которая полная цепочка сертификатов не был импортирован в доверенные хранилища.Импортируйте сертификаты с помощью утилиты keytool прямо из корневого сертификата. Вы также можете открыть файл cacerts в блокноте и посмотреть, импортирована ли полная цепочка сертификатов или нет.Проверьте псевдоним, который вы указали при импорте сертификатов, откройте сертификаты и посмотрите, сколько они содержат. Такое же количество сертификатов должно быть в файле cacerts.
Также файл cacerts должен быть настроен на сервере, на котором вы запускаете свое приложение, два сервера будут аутентифицировать друг друга с помощью открытых/закрытых ключей.