HTTPS с аутентификацией клиента не работает на Android
В настоящее время я пишу приложение для Android (Min SDK 16), которое запрашивает данные у HTTPS-сервера. Сервер (Apache 2.4 на Debian 8) использует сертификат, подписанный нашим собственным CA, и требует, чтобы клиенты также имели сертификат, подписанный им. Это прекрасно работает с Firefox после импорта и CA, и сертификата клиента в формате PKCS.
Однако я не могу заставить это работать в Android. Я использую HttpsURLConnections, так как HTTP-клиент Apache недавно устарел для Android. Доверяю нашему пользовательскому центру сертификации, но как только мне требуется сертификат клиента, я получаю следующее исключение:
java.lang.reflect.InvocationTargetException
[...]
Caused by: java.security.cert.CertPathValidatorException: Trust anchor for certification path not found.
at com.android.org.conscrypt.TrustManagerImpl.checkTrusted(TrustManagerImpl.java:282)
at com.android.org.conscrypt.TrustManagerImpl.checkServerTrusted(TrustManagerImpl.java:192)
at eu.olynet.olydorfapp.resources.CustomTrustManager.checkServerTrusted(CustomTrustManager.java:96)
at com.android.org.conscrypt.OpenSSLSocketImpl.verifyCertificateChain(OpenSSLSocketImpl.java:614)
at com.android.org.conscrypt.NativeCrypto.SSL_do_handshake(Native Method)
at com.android.org.conscrypt.OpenSSLSocketImpl.startHandshake(OpenSSLSocketImpl.java:406)
at com.android.okhttp.Connection.upgradeToTls(Connection.java:146)
at com.android.okhttp.Connection.connect(Connection.java:107)
at com.android.okhttp.internal.http.HttpEngine.connect(HttpEngine.java:294)
at com.android.okhttp.internal.http.HttpEngine.sendSocketRequest(HttpEngine.java:255)
at com.android.okhttp.internal.http.HttpEngine.sendRequest(HttpEngine.java:206)
at com.android.okhttp.internal.http.HttpURLConnectionImpl.execute(HttpURLConnectionImpl.java:345)
at com.android.okhttp.internal.http.HttpURLConnectionImpl.getResponse(HttpURLConnectionImpl.java:296)
at com.android.okhttp.internal.http.HttpURLConnectionImpl.getResponseCode(HttpURLConnectionImpl.java:503)
at com.android.okhttp.internal.http.HttpsURLConnectionImpl.getResponseCode(HttpsURLConnectionImpl.java:136)
at org.jboss.resteasy.client.jaxrs.engines.URLConnectionEngine.invoke(URLConnectionEngine.java:49)
at org.jboss.resteasy.client.jaxrs.internal.ClientInvocation.invoke(ClientInvocation.java:436)
at org.jboss.resteasy.client.jaxrs.internal.proxy.ClientInvoker.invoke(ClientInvoker.java:102)
at org.jboss.resteasy.client.jaxrs.internal.proxy.ClientProxy.invoke(ClientProxy.java:64)
at $Proxy9.getMetaNews(Native Method)
at java.lang.reflect.Method.invokeNative(Native Method)
at java.lang.reflect.Method.invoke(Method.java:515)
at eu.olynet.olydorfapp.resources.ResourceManager.fetchMetaItems(ResourceManager.java:372)
at eu.olynet.olydorfapp.resources.ResourceManager.getTreeOfMetaItems(ResourceManager.java:542)
at eu.olynet.olydorfapp.tabs.NewsTab$1.doInBackground(NewsTab.java:51)
at eu.olynet.olydorfapp.tabs.NewsTab$1.doInBackground(NewsTab.java:45)
at android.os.AsyncTask$2.call(AsyncTask.java:288)
at java.util.concurrent.FutureTask.run(FutureTask.java:237)
at android.os.AsyncTask$SerialExecutor$1.run(AsyncTask.java:231)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1112)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:587)
at java.lang.Thread.run(Thread.java:841)
Для меня это выглядит как сертификат сервера не может быть проверено, что не должно быть.
Вот как выглядит код:
private static final String CA_FILE = "ca.pem";
private static final String CERTIFICATE_FILE = "app_01.pfx";
private static final char[] CERTIFICATE_KEY = "password".toCharArray();
[...]
CertificateFactory cf = CertificateFactory.getInstance("X.509");
String algorithm = TrustManagerFactory.getDefaultAlgorithm();
InputStream ca = this.context.getAssets().open(CA_FILE);
KeyStore trustStore = KeyStore.getInstance("PKCS12");
trustStore.load(null);
Certificate caCert = cf.generateCertificate(ca);
trustStore.setCertificateEntry("CA Name", caCert);
CustomTrustManager tm = new CustomTrustManager(trustStore);
ca.close();
InputStream clientCert = this.context.getAssets().open(CERTIFICATE_FILE);
KeyStore keyStore = KeyStore.getInstance("PKCS12");
keyStore.load(clientCert, CERTIFICATE_KEY);
Log.e("KeyStore", "Size: " + keyStore.size());
KeyManagerFactory kmf = KeyManagerFactory.getInstance(algorithm);
kmf.init(keyStore, CERTIFICATE_KEY);
clientCert.close();
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(kmf.getKeyManagers(), new TrustManager[]{tm}, null);
[...]
((HttpsURLConnection) con).setSSLSocketFactory(sslContext.getSocketFactory());
Соответствующая функция CustomTrustManager (где localTrustManager содержит только наш CA и defaultTrustManager CA системы):
public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
try {
localTrustManager.checkServerTrusted(chain, authType);
} catch (CertificateException ce) {
defaultTrustManager.checkServerTrusted(chain, authType);
}
}
Я уже пытался преобразовать файл PKCS в файл BKS (и, конечно, адаптировать KeyStore), но безуспешно. Я также видел подобные вопросы здесь, но ни одно из решений не помогло мне.
1 ответ
Я обнаружил, что добавление промежуточного ЦС (того, который подписал сертификат сервера напрямую) в дополнение к корневому ЦС работало. Я не понимаю, почему это необходимо, поскольку проверка работает нормально только с корневым центром сертификации, если сервер не требует сертификата клиента. Мне кажется, это какая-то ошибка в реализации HttpsURLConnections для Android или связанного класса. Пожалуйста, просветите меня, если я ошибаюсь.
Рабочий код:
private static final String CA_FILE = "ca.pem";
private static final String INTERMEDIATE_FILE = "intermediate.pem";
private static final String CERTIFICATE_FILE = "app_01.pfx";
private static final char[] CERTIFICATE_KEY = "password".toCharArray();
[...]
CertificateFactory cf = CertificateFactory.getInstance("X.509");
String algorithm = TrustManagerFactory.getDefaultAlgorithm();
/* trust setup */
InputStream ca = this.context.getAssets().open(CA_FILE);
InputStream intermediate = this.context.getAssets().open(INTERMEDIATE_FILE);
KeyStore trustStore = KeyStore.getInstance("PKCS12");
trustStore.load(null);
Certificate caCert = cf.generateCertificate(ca);
Certificate intermediateCert = cf.generateCertificate(intermediate);
trustStore.setCertificateEntry("CA Name", caCert);
trustStore.setCertificateEntry("Intermediate Name", intermediateCert);
CustomTrustManager tm = new CustomTrustManager(trustStore);
ca.close();
intermediate.close();
/* client certificate setup */
InputStream clientCert = this.context.getAssets().open(CERTIFICATE_FILE);
KeyStore keyStore = KeyStore.getInstance("PKCS12");
keyStore.load(clientCert, CERTIFICATE_KEY);
KeyManagerFactory kmf = KeyManagerFactory.getInstance(algorithm);
kmf.init(keyStore, CERTIFICATE_KEY);
clientCert.close();
/* SSLContext setup */
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(kmf.getKeyManagers(), new TrustManager[]{tm}, null);
[...]
((HttpsURLConnection) con).setSSLSocketFactory(sslContext.getSocketFactory());