Вопрос-ответ

Trusting all certificates using HttpClient over HTTPS

Доверяем всем сертификатам с помощью HttpClient через HTTPS

Недавно опубликовал вопрос, касающийся HttpClient через Https (нашел здесь). Я добился некоторого прогресса, но столкнулся с новыми проблемами. Как и в случае с моей последней проблемой, я нигде не могу найти пример, который мне подходит. По сути, я хочу, чтобы мой клиент принимал любой сертификат (потому что я всегда указываю только на один сервер), но я продолжаю получать javax.net.ssl.SSLException: Not trusted server certificate exception.

Итак, это то, что у меня есть:


public void connect() throws A_WHOLE_BUNCH_OF_EXCEPTIONS {

HttpPost post = new HttpPost(new URI(PROD_URL));
post.setEntity(new StringEntity(BODY));

KeyStore trusted = KeyStore.getInstance("BKS");
trusted.load(null, "".toCharArray());
SSLSocketFactory sslf = new SSLSocketFactory(trusted);
sslf.setHostnameVerifier(SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER);

SchemeRegistry schemeRegistry = new SchemeRegistry();
schemeRegistry.register(new Scheme ("https", sslf, 443));
SingleClientConnManager cm = new SingleClientConnManager(post.getParams(),
schemeRegistry);

HttpClient client = new DefaultHttpClient(cm, post.getParams());
HttpResponse result = client.execute(post);
}

И вот ошибка, которую я получаю:

    W/System.err(  901): javax.net.ssl.SSLException: Not trusted server certificate 
W/System.err( 901): at org.apache.harmony.xnet.provider.jsse.OpenSSLSocketImpl.startHandshake(OpenSSLSocketImpl.java:360)
W/System.err( 901): at org.apache.http.conn.ssl.AbstractVerifier.verify(AbstractVerifier.java:92)
W/System.err( 901): at org.apache.http.conn.ssl.SSLSocketFactory.connectSocket(SSLSocketFactory.java:321)
W/System.err( 901): at org.apache.http.impl.conn.DefaultClientConnectionOperator.openConnection(DefaultClientConnectionOperator.java:129)
W/System.err( 901): at org.apache.http.impl.conn.AbstractPoolEntry.open(AbstractPoolEntry.java:164)
W/System.err( 901): at org.apache.http.impl.conn.AbstractPooledConnAdapter.open(AbstractPooledConnAdapter.java:119)
W/System.err( 901): at org.apache.http.impl.client.DefaultRequestDirector.execute(DefaultRequestDirector.java:348)
W/System.err( 901): at org.apache.http.impl.client.AbstractHttpClient.execute(AbstractHttpClient.java:555)
W/System.err( 901): at org.apache.http.impl.client.AbstractHttpClient.execute(AbstractHttpClient.java:487)
W/System.err( 901): at org.apache.http.impl.client.AbstractHttpClient.execute(AbstractHttpClient.java:465)
W/System.err( 901): at me.harrisonlee.test.ssl.MainActivity.connect(MainActivity.java:129)
W/System.err( 901): at me.harrisonlee.test.ssl.MainActivity.access$0(MainActivity.java:77)
W/System.err( 901): at me.harrisonlee.test.ssl.MainActivity$2.run(MainActivity.java:49)
W/System.err( 901): Caused by: java.security.cert.CertificateException: java.security.InvalidAlgorithmParameterException: the trust anchors set is empty
W/System.err( 901): at org.apache.harmony.xnet.provider.jsse.TrustManagerImpl.checkServerTrusted(TrustManagerImpl.java:157)
W/System.err( 901): at org.apache.harmony.xnet.provider.jsse.OpenSSLSocketImpl.startHandshake(OpenSSLSocketImpl.java:355)
W/System.err( 901): ... 12 more
W/System.err( 901): Caused by: java.security.InvalidAlgorithmParameterException: the trust anchors set is empty
W/System.err( 901): at java.security.cert.PKIXParameters.checkTrustAnchors(PKIXParameters.java:645)
W/System.err( 901): at java.security.cert.PKIXParameters.<init>(PKIXParameters.java:89)
W/System.err( 901): at org.apache.harmony.xnet.provider.jsse.TrustManagerImpl.<init>(TrustManagerImpl.java:89)
W/System.err( 901): at org.apache.harmony.xnet.provider.jsse.TrustManagerFactoryImpl.engineGetTrustManagers(TrustManagerFactoryImpl.java:134)
W/System.err( 901): at javax.net.ssl.TrustManagerFactory.getTrustManagers(TrustManagerFactory.java:226)W/System.err( 901): at org.apache.http.conn.ssl.SSLSocketFactory.createTrustManagers(SSLSocketFactory.java:263)
W/System.err( 901): at org.apache.http.conn.ssl.SSLSocketFactory.<init>(SSLSocketFactory.java:190)
W/System.err( 901): at org.apache.http.conn.ssl.SSLSocketFactory.<init>(SSLSocketFactory.java:216)
W/System.err( 901): at me.harrisonlee.test.ssl.MainActivity.connect(MainActivity.java:107)
W/System.err( 901): ... 2 more
Переведено автоматически
Ответ 1

В принципе, у вас есть четыре потенциальных решения для исправления "ненадежного" исключения на Android с помощью httpclient:


  1. Доверяем всем сертификатам. Не делайте этого, если вы действительно не знаете, что делаете.

  2. Создайте пользовательский SSLSocketFactory, который доверяет только вашему сертификату. Это работает до тех пор, пока вы точно знаете, к каким серверам собираетесь подключаться, но как только вам понадобится подключиться к новому серверу с другим SSL-сертификатом, вам потребуется обновить ваше приложение.

  3. Создайте файл хранилища ключей, содержащий "основной список" сертификатов Android, затем добавьте свой собственный. Если срок действия какого-либо из этих сертификатов истекает в будущем, вы несете ответственность за их обновление в своем приложении. Я не могу придумать причину для этого.

  4. Создайте пользовательский SSLSocketFactory, который использует встроенное хранилище ключей сертификатов, но использует альтернативное хранилище ключей для всего, что не удается проверить с помощью хранилища по умолчанию.

В этом ответе используется решение № 4, которое мне кажется наиболее надежным.

Решение заключается в использовании SSLSocketFactory, который может принимать несколько хранилищ ключей, позволяя вам предоставлять вашему собственному хранилищу ключей ваши собственные сертификаты. Это позволяет загружать дополнительные сертификаты верхнего уровня, такие как Thawte, которые могут отсутствовать на некоторых устройствах Android. Это также позволяет загружать ваши собственные самозаверяющие сертификаты. Сначала будут использоваться встроенные сертификаты устройств по умолчанию, и ваши дополнительные сертификаты будут использоваться только по мере необходимости.

Сначала вам нужно определить, какой сертификат отсутствует в вашем хранилище ключей. Выполните следующую команду:

openssl s_client -connect www.yourserver.com:443

И вы увидите результат, подобный следующему:

Certificate chain
0 s:/O=www.yourserver.com/OU=Go to
https://www.thawte.com/repository/index.html/OU=Thawte SSL123
certificate/OU=Domain Validated/CN=www.yourserver.com
i:/C=US/O=Thawte, Inc./OU=Domain Validated SSL/CN=Thawte DV SSL CA
1 s:/C=US/O=Thawte, Inc./OU=Domain Validated SSL/CN=Thawte DV SSL CA
i:/C=US/O=thawte, Inc./OU=Certification Services Division/OU=(c)
2006 thawte, Inc. - For authorized use only/CN=thawte Primary Root CA

Как вы можете видеть, наш корневой сертификат от Thawte. Перейдите на веб-сайт вашего провайдера и найдите соответствующий сертификат. Для нас это было здесь, и вы можете видеть, что нам нужен был one Copyright 2006.

Если вы используете самозаверяющий сертификат, вам не нужно выполнять предыдущий шаг, поскольку у вас уже есть сертификат подписи.

Затем создайте файл хранилища ключей, содержащий отсутствующий сертификат подписи. У Crazybob есть подробные сведения о том, как это сделать на Android, но идея состоит в том, чтобы сделать следующее:

Если у вас ее еще нет, загрузите библиотеку bouncy castle provider с: http://www.bouncycastle.org/latest_releases.html. Это будет указано в вашем пути к классу ниже.

Запустите команду для извлечения сертификата с сервера и создайте pem-файл. В данном случае mycert.pem.

echo | openssl s_client -connect ${MY_SERVER}:443 2>&1 | \
sed -ne '/-BEGIN CERTIFICATE-/,/-END CERTIFICATE-/p' > mycert.pem

Затем выполните следующие команды для создания хранилища ключей.

export CLASSPATH=/path/to/bouncycastle/bcprov-jdk15on-155.jar
CERTSTORE=res/raw/mystore.bks
if [ -a $CERTSTORE ]; then
rm $CERTSTORE || exit 1
fi
keytool \
-import \
-v \
-trustcacerts \
-alias 0 \
-file <(openssl x509 -in mycert.pem) \
-keystore $CERTSTORE \
-storetype BKS \
-provider org.bouncycastle.jce.provider.BouncyCastleProvider \
-providerpath /path/to/bouncycastle/bcprov-jdk15on-155.jar \
-storepass some-password

Вы заметите, что приведенный выше скрипт помещает результат в res/raw/mystore.bks. Теперь у вас есть файл, который вы будете загружать в свое приложение для Android и который предоставляет недостающие сертификаты.

Для этого зарегистрируйте свой SSLSocketFactory для схемы SSL:

final SchemeRegistry schemeRegistry = new SchemeRegistry();
schemeRegistry.register(new Scheme("http", PlainSocketFactory.getSocketFactory(), 80));
schemeRegistry.register(new Scheme("https", createAdditionalCertsSSLSocketFactory(), 443));

// and then however you create your connection manager, I use ThreadSafeClientConnManager
final HttpParams params = new BasicHttpParams();
...
final ThreadSafeClientConnManager cm = new ThreadSafeClientConnManager(params,schemeRegistry);

Чтобы создать свой SSLSocketFactory:

protected org.apache.http.conn.ssl.SSLSocketFactory createAdditionalCertsSSLSocketFactory() {
try {
final KeyStore ks = KeyStore.getInstance("BKS");

// the bks file we generated above
final InputStream in = context.getResources().openRawResource( R.raw.mystore);
try {
// don't forget to put the password used above in strings.xml/mystore_password
ks.load(in, context.getString( R.string.mystore_password ).toCharArray());
} finally {
in.close();
}

return new AdditionalKeyStoresSSLSocketFactory(ks);

} catch( Exception e ) {
throw new RuntimeException(e);
}
}

И, наконец, дополнительный код keystoresslsocketfactory, который принимает ваше новое хранилище ключей и проверяет, не удается ли встроенному хранилищу ключей подтвердить SSL-сертификат:

/**
* Allows you to trust certificates from additional KeyStores in addition to
* the default KeyStore
*/

public class AdditionalKeyStoresSSLSocketFactory extends SSLSocketFactory {
protected SSLContext sslContext = SSLContext.getInstance("TLS");

public AdditionalKeyStoresSSLSocketFactory(KeyStore keyStore) throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException, UnrecoverableKeyException {
super(null, null, null, null, null, null);
sslContext.init(null, new TrustManager[]{new AdditionalKeyStoresTrustManager(keyStore)}, null);
}

@Override
public Socket createSocket(Socket socket, String host, int port, boolean autoClose) throws IOException {
return sslContext.getSocketFactory().createSocket(socket, host, port, autoClose);
}

@Override
public Socket createSocket() throws IOException {
return sslContext.getSocketFactory().createSocket();
}



/**
* Based on http://download.oracle.com/javase/1.5.0/docs/guide/security/jsse/JSSERefGuide.html#X509TrustManager
*/

public static class AdditionalKeyStoresTrustManager implements X509TrustManager {

protected ArrayList<X509TrustManager> x509TrustManagers = new ArrayList<X509TrustManager>();


protected AdditionalKeyStoresTrustManager(KeyStore... additionalkeyStores) {
final ArrayList<TrustManagerFactory> factories = new ArrayList<TrustManagerFactory>();

try {
// The default Trustmanager with default keystore
final TrustManagerFactory original = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
original.init((KeyStore) null);
factories.add(original);

for( KeyStore keyStore : additionalkeyStores ) {
final TrustManagerFactory additionalCerts = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
additionalCerts.init(keyStore);
factories.add(additionalCerts);
}

} catch (Exception e) {
throw new RuntimeException(e);
}



/*
* Iterate over the returned trustmanagers, and hold on
* to any that are X509TrustManagers
*/

for (TrustManagerFactory tmf : factories)
for( TrustManager tm : tmf.getTrustManagers() )
if (tm instanceof X509TrustManager)
x509TrustManagers.add( (X509TrustManager)tm );


if( x509TrustManagers.size()==0 )
throw new RuntimeException("Couldn't find any X509TrustManagers");

}

/*
* Delegate to the default trust manager.
*/

public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
final X509TrustManager defaultX509TrustManager = x509TrustManagers.get(0);
defaultX509TrustManager.checkClientTrusted(chain, authType);
}

/*
* Loop over the trustmanagers until we find one that accepts our server
*/

public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
for( X509TrustManager tm : x509TrustManagers ) {
try {
tm.checkServerTrusted(chain,authType);
return;
} catch( CertificateException e ) {
// ignore
}
}
throw new CertificateException();
}

public X509Certificate[] getAcceptedIssuers() {
final ArrayList<X509Certificate> list = new ArrayList<X509Certificate>();
for( X509TrustManager tm : x509TrustManagers )
list.addAll(Arrays.asList(tm.getAcceptedIssuers()));
return list.toArray(new X509Certificate[list.size()]);
}
}

}
Ответ 2

Примечание: Не реализуйте это в производственном коде, который вы когда-либо собираетесь использовать в сети, которой вы не полностью доверяете. Особенно во всем, что происходит через общедоступный Интернет.

Ваш вопрос - это как раз то, что я хочу знать. После того, как я провел некоторые поиски, вывод таков.

Способом HttpClient вы должны создать пользовательский класс из org.apache.http.conn.ssl.SSLSocketFactory, а не из самого org.apache.http.conn.ssl.SSLSocketFactory. Некоторые подсказки можно найти в этом посте Пользовательская обработка SSL перестала работать на Android 2.2 FroYo.

Пример такой ...

import java.io.IOException;
import java.net.Socket;
import java.net.UnknownHostException;
import java.security.KeyManagementException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.UnrecoverableKeyException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;

import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;

import org.apache.http.conn.ssl.SSLSocketFactory;
public class MySSLSocketFactory extends SSLSocketFactory {
SSLContext sslContext = SSLContext.getInstance("TLS");

public MySSLSocketFactory(KeyStore truststore) throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException, UnrecoverableKeyException {
super(truststore);

TrustManager tm = new X509TrustManager() {
public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
}

public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
}

public X509Certificate[] getAcceptedIssuers() {
return null;
}
};

sslContext.init(null, new TrustManager[] { tm }, null);
}

@Override
public Socket createSocket(Socket socket, String host, int port, boolean autoClose) throws IOException, UnknownHostException {
return sslContext.getSocketFactory().createSocket(socket, host, port, autoClose);
}

@Override
public Socket createSocket() throws IOException {
return sslContext.getSocketFactory().createSocket();
}
}

и используем этот класс при создании экземпляра HttpClient.

public HttpClient getNewHttpClient() {
try {
KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType());
trustStore.load(null, null);

MySSLSocketFactory sf = new MySSLSocketFactory(trustStore);
sf.setHostnameVerifier(SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER);

HttpParams params = new BasicHttpParams();
HttpProtocolParams.setVersion(params, HttpVersion.HTTP_1_1);
HttpProtocolParams.setContentCharset(params, HTTP.UTF_8);

SchemeRegistry registry = new SchemeRegistry();
registry.register(new Scheme("http", PlainSocketFactory.getSocketFactory(), 80));
registry.register(new Scheme("https", sf, 443));

ClientConnectionManager ccm = new ThreadSafeClientConnManager(params, registry);

return new DefaultHttpClient(ccm, params);
} catch (Exception e) {
return new DefaultHttpClient();
}
}

Кстати, ссылка ниже предназначена для тех, кто ищет решение HttpURLConnection.
Https-соединение Android

Я протестировал два вышеупомянутых типа решений на froyo, и все они прекрасно работают в моих случаях. Наконец, при использовании HttpURLConnection могут возникнуть проблемы с перенаправлением, но это выходит за рамки темы.

Примечание: Прежде чем вы решите доверять всем сертификатам, вы, вероятно, должны хорошо знать сайт и не навредить конечному пользователю.

Действительно, следует тщательно взвесить риск, на который вы идете, включая эффект от макета сайта хакера, упомянутого в следующих комментариях, которые я глубоко оценил. В некоторых ситуациях, хотя может быть сложно позаботиться обо всех сертификатах, вам лучше знать о неявных недостатках доверия ко всем из них.

Ответ 3

Добавьте этот код перед HttpsURLConnection и это будет сделано. Я понял.

private void trustEveryone() { 
try {
HttpsURLConnection.setDefaultHostnameVerifier(new HostnameVerifier(){
public boolean verify(String hostname, SSLSession session) {
return true;
}});
SSLContext context = SSLContext.getInstance("TLS");
context.init(null, new X509TrustManager[]{new X509TrustManager(){
public void checkClientTrusted(X509Certificate[] chain,
String authType)
throws CertificateException {}
public void checkServerTrusted(X509Certificate[] chain,
String authType)
throws CertificateException {}
public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[0];
}}}, new SecureRandom());
HttpsURLConnection.setDefaultSSLSocketFactory(
context.getSocketFactory());
} catch (Exception e) { // should never happen
e.printStackTrace();
}
}
Ответ 4

Это плохая идея. Доверять любому сертификату лишь (очень) немного лучше, чем вообще не использовать SSL. Когда вы говорите "Я хочу, чтобы мой клиент принял любой сертификат (потому что я всегда указываю только на один сервер)", вы предполагаете, что это означает, что каким-то образом указание на "один сервер" безопасно, чего нет в общедоступной сети.

Вы полностью открыты для атаки "человек посередине", доверяя любому сертификату. Любой может проксировать ваше соединение, установив отдельное SSL-соединение с вами и с конечным сервером. Затем MITM получает доступ ко всему вашему запросу и ответу. Если вам изначально не был нужен SSL (в вашем сообщении нет ничего конфиденциального и не выполняется аутентификация), вам не следует слепо доверять всем сертификатам.

Вам следует рассмотреть возможность добавления общедоступного сертификата в jks с помощью keytool и использовать его для создания вашей фабрики сокетов, например, этого:

    KeyStore ks = KeyStore.getInstance("JKS");

// get user password and file input stream
char[] password = ("mykspassword")).toCharArray();
ClassLoader cl = this.getClass().getClassLoader();
InputStream stream = cl.getResourceAsStream("myjks.jks");
ks.load(stream, password);
stream.close();

SSLContext sc = SSLContext.getInstance("TLS");
KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509");
TrustManagerFactory tmf = TrustManagerFactory.getInstance("SunX509");

kmf.init(ks, password);
tmf.init(ks);

sc.init(kmf.getKeyManagers(), tmf.getTrustManagers(),null);

return sc.getSocketFactory();

Здесь есть одно предостережение, на которое следует обратить внимание. Срок действия сертификата со временем истечет, и код перестанет работать в это время. Вы можете легко определить, когда это произойдет, посмотрев на сертификат.

java