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());
Другие вопросы по тегам