Программно добавить центр сертификации, сохраняя сертификаты SSL системы Android
В StackOverflow есть много вопросов по этой теме, но я, похоже, не нашел связанных с моей проблемой.
У меня есть приложение для Android, которое должно взаимодействовать с серверами HTTPS: некоторые из них подписаны с CA, зарегистрированным в хранилище ключей Android (общие веб-сайты HTTPS), а некоторые из них подписаны с CA, которым я владею, но не в хранилище ключей системы Android (a сервер с авторизованным сертификатом, например).
Я знаю, как добавить свой CA программно и заставить каждое HTTPS-соединение использовать его. Я использую следующий код:
public class SslCertificateAuthority {
public static void addCertificateAuthority(InputStream inputStream) {
try {
// Load CAs from an InputStream
// (could be from a resource or ByteArrayInputStream or ...)
CertificateFactory cf = CertificateFactory.getInstance("X.509");
InputStream caInput = new BufferedInputStream(inputStream);
Certificate ca;
try {
ca = cf.generateCertificate(caInput);
} finally {
caInput.close();
}
// Create a KeyStore containing our trusted CAs
String keyStoreType = KeyStore.getDefaultType();
KeyStore keyStore = KeyStore.getInstance(keyStoreType);
keyStore.load(null, null);
keyStore.setCertificateEntry("ca", ca);
// Create a TrustManager that trusts the CAs in our KeyStore
String tmfAlgorithm = TrustManagerFactory.getDefaultAlgorithm();
TrustManagerFactory tmf = TrustManagerFactory.getInstance(tmfAlgorithm);
tmf.init(keyStore);
// Create an SSLContext that uses our TrustManager
SSLContext context = SSLContext.getInstance("TLS");
context.init(null, tmf.getTrustManagers(), null);
// Tell the URLConnection to use a SocketFactory from our SSLContext
HttpsURLConnection.setDefaultSSLSocketFactory(context.getSocketFactory());
} catch (CertificateException e) {
e.printStackTrace();
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (KeyStoreException e) {
e.printStackTrace();
} catch (KeyManagementException e) {
e.printStackTrace();
} catch (MalformedURLException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
Однако это делает невозможным использование хранилища ключей системы Android, и я больше не могу запрашивать сайты HTTPS, подписанные с другим CA.
Я попытался добавить свой CA в хранилище ключей Android, используя:
KeyStore.getInstance("AndroidCAStore")
... но я не могу потом добавить в него свой CA (исключение запускается).
Я мог бы использовать метод экземпляра HttpsURLConnection.setSSLSocketFactory(...)
вместо статического глобального HttpsURLConnection.setDefaultSSLSocketFactory(...)
, чтобы рассказать в каждом конкретном случае, когда мой CA должен использоваться.
Но это совсем не практично, тем более, что иногда я не могу передать предварительно сконфигурированный объект HttpsURLConnection
в некоторые библиотеки.
Некоторые идеи о том, как я могу это сделать?
EDIT - ANSWER
Хорошо, следуя приведенному совету, вот мой рабочий код. Возможно, это потребует некоторых улучшений, но, похоже, это является отправной точкой.
public class SslCertificateAuthority {
private static class UnifiedTrustManager implements X509TrustManager {
private X509TrustManager defaultTrustManager;
private X509TrustManager localTrustManager;
public UnifiedTrustManager(KeyStore localKeyStore) throws KeyStoreException {
try {
this.defaultTrustManager = createTrustManager(null);
this.localTrustManager = createTrustManager(localKeyStore);
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
}
private X509TrustManager createTrustManager(KeyStore store) throws NoSuchAlgorithmException, KeyStoreException {
String tmfAlgorithm = TrustManagerFactory.getDefaultAlgorithm();
TrustManagerFactory tmf = TrustManagerFactory.getInstance(tmfAlgorithm);
tmf.init((KeyStore) store);
TrustManager[] trustManagers = tmf.getTrustManagers();
return (X509TrustManager) trustManagers[0];
}
public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
try {
defaultTrustManager.checkServerTrusted(chain, authType);
} catch (CertificateException ce) {
localTrustManager.checkServerTrusted(chain, authType);
}
}
@Override
public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
try {
defaultTrustManager.checkClientTrusted(chain, authType);
} catch (CertificateException ce) {
localTrustManager.checkClientTrusted(chain, authType);
}
}
@Override
public X509Certificate[] getAcceptedIssuers() {
X509Certificate[] first = defaultTrustManager.getAcceptedIssuers();
X509Certificate[] second = localTrustManager.getAcceptedIssuers();
X509Certificate[] result = Arrays.copyOf(first, first.length + second.length);
System.arraycopy(second, 0, result, first.length, second.length);
return result;
}
}
public static void setCustomCertificateAuthority(InputStream inputStream) {
try {
// Load CAs from an InputStream
// (could be from a resource or ByteArrayInputStream or ...)
CertificateFactory cf = CertificateFactory.getInstance("X.509");
InputStream caInput = new BufferedInputStream(inputStream);
Certificate ca;
try {
ca = cf.generateCertificate(caInput);
System.out.println("ca=" + ((X509Certificate) ca).getSubjectDN());
} finally {
caInput.close();
}
// Create a KeyStore containing our trusted CAs
String keyStoreType = KeyStore.getDefaultType();
KeyStore keyStore = KeyStore.getInstance(keyStoreType);
keyStore.load(null, null);
keyStore.setCertificateEntry("ca", ca);
// Create a TrustManager that trusts the CAs in our KeyStore and system CA
UnifiedTrustManager trustManager = new UnifiedTrustManager(keyStore);
// Create an SSLContext that uses our TrustManager
SSLContext context = SSLContext.getInstance("TLS");
context.init(null, new TrustManager[]{trustManager}, null);
// Tell the URLConnection to use a SocketFactory from our SSLContext
HttpsURLConnection.setDefaultSSLSocketFactory(context.getSocketFactory());
} catch (CertificateException e) {
e.printStackTrace();
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (KeyStoreException e) {
e.printStackTrace();
} catch (KeyManagementException e) {
e.printStackTrace();
} catch (MalformedURLException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
Ответы
Ответ 1
Попробуйте реализовать настраиваемый диспетчер доверия, чтобы он проверял ваши пользовательские сертификаты и если это не справляется с встроенными сертификатами Android.
Взгляните на эту статью: Использование специализированного хранилища сертификатов для сертификатов на Android.
Я думаю, что параграф "Создание динамического TrustManager" обрабатывает именно то, что вы просите.
Ответ 2
Это старый вопрос, но я столкнулся с той же проблемой, поэтому, вероятно, стоит опубликовать мой ответ. Вы попытались добавить свой сертификат в KeyStore.getInstance("AndroidCAStore")
, но получили исключение. На самом деле вы должны были сделать обратное - добавить записи из этого хранилища ключей в тот, который вы создали. Мой код немного отличается от вашего, я просто публикую его ради полного ответа, хотя имеет значение только средняя часть.
KeyStore keyStore=KeyStore.getInstance("BKS");
InputStream in=activity.getResources().openRawResource(R.raw.my_ca);
try
{
keyStore.load(in,"PASSWORD_HERE".toCharArray());
}
finally
{
in.close();
}
KeyStore defaultCAs=KeyStore.getInstance("AndroidCAStore");
if(defaultCAs!=null)
{
defaultCAs.load(null,null);
Enumeration<String> keyAliases=defaultCAs.aliases();
while(keyAliases.hasMoreElements())
{
String alias=keyAliases.nextElement();
Certificate cert=defaultCAs.getCertificate(alias);
try
{
if(!keyStore.containsAlias(alias))
keyStore.setCertificateEntry(alias,cert);
}
catch(Exception e)
{
System.out.println("Error adding "+e);
}
}
}
TrustManagerFactory tmf=TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
tmf.init(keyStore);
// Get a new SSL context
SSLContext ctx = SSLContext.getInstance("SSL");
ctx.init(null,tmf.getTrustManagers(),new java.security.SecureRandom());
return ctx.getSocketFactory();
Ответ 3
Это может быть слишком поздно, но это проверенный и проверенный подход, который помогает обойти проверку сертификата, выполняемую Java.
Я не могу претендовать на кредит для этого кода, он был написан одним из моих коллег:). Его можно использовать во время разработки, чтобы проверить ваш код. Если вы вообще не хотите иметь сертификаты, вы можете сделать Java всегда сертификаты с любого хоста для вашего объекта HttpURLConnection. Кажется, это именно то, что вы пытаетесь сделать здесь.
Здесь класс, который должен помочь вам сделать это:
import javax.net.ssl.*;
import java.net.HttpURLConnection;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
/***
* Should only be used in development, this class will allow connections to an HTTPS server with unverified certificates.
* obviously this should not be used in the real world
*/
public class TrustModifier {
private static final TrustingHostnameVerifier TRUSTING_HOSTNAME_VERIFIER = new TrustingHostnameVerifier();
private static SSLSocketFactory factory;
/**
* Call this with any HttpURLConnection, and it will modify the trust settings if it is an HTTPS connection.
*
* @param conn the {@link HttpURLConnection} instance
* @throws KeyManagementException if an error occurs while initializing the context object for the TLS protocol
* @throws NoSuchAlgorithmException if no Provider supports a TrustManagerFactorySpi implementation for the TLS protocol.
*/
public static void relaxHostChecking(HttpURLConnection conn) throws KeyManagementException, NoSuchAlgorithmException {
if (conn instanceof HttpsURLConnection) {
HttpsURLConnection httpsConnection = (HttpsURLConnection) conn;
SSLSocketFactory factory = prepFactory();
httpsConnection.setSSLSocketFactory(factory);
httpsConnection.setHostnameVerifier(TRUSTING_HOSTNAME_VERIFIER);
}
}
/**
* Returns an {@link SSLSocketFactory} instance for the protocol being passed, this represents a secure communication context
*
* @return a {@link SSLSocketFactory} object for the TLS protocol
* @throws NoSuchAlgorithmException if no Provider supports a TrustManagerFactorySpi implementation for the specified protocol.
* @throws KeyManagementException if an error occurs while initializing the context object
*/
static synchronized SSLSocketFactory prepFactory() throws NoSuchAlgorithmException, KeyManagementException {
if (factory == null) {
SSLContext ctx = SSLContext.getInstance("TLS");
ctx.init(null, new TrustManager[]{new AlwaysTrustManager()}, null);
factory = ctx.getSocketFactory();
}
return factory;
}
private static final class TrustingHostnameVerifier implements HostnameVerifier {
public boolean verify(String hostname, SSLSession session) {
return true;
}
}
private static class AlwaysTrustManager implements X509TrustManager {
public void checkClientTrusted(X509Certificate[] arg0, String arg1) throws CertificateException {
}
public void checkServerTrusted(X509Certificate[] arg0, String arg1) throws CertificateException {
}
public X509Certificate[] getAcceptedIssuers() {
return null;
}
}
}
Все, что вам нужно сделать, это вызвать функцию relaxHostChecking() следующим образом:
if (conn instanceof HttpsURLConnection) {
TrustModifier.relaxHostChecking(conn);
}
Это приведет к тому, что java доверяет любому хосту, с которым вы пытаетесь подключиться к HttpURLConnection.