Ошибка "Нет сертификата пера" в Android 2.3, но НЕ в 4
Получение "javax.net.ssl.SSLPeerUnverifiedException: No peer certificate error"
в эмуляторе под управлением Android 2.3, но не в 4. В 4 он работает отлично. Я пытаюсь подключиться к живому серверу через https. Он использует действующий сертификат Thawte, отлично работает во всех браузерах и Android 3 и 4.
Если у кого-то есть помощь по коду, ПОЖАЛУЙСТА и спасибо. Кроме того, если у кого-нибудь есть какие-либо предложения по безопасному обходному пути, я был бы признателен за это. Я все еще учусь, и я был над этой проблемой в течение недели. Это должно закончиться, поэтому я могу продолжать работать и учиться. Urgh.
Вот код HttpCLient, любезно предоставленный Антуаном Хауком ( http://blog.antoine.li/2010/10/22/android-trusting-ssl-certificates/):
import java.io.InputStream;
import java.security.KeyStore;
import java.security.cert.CertificateException;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import javax.security.cert.X509Certificate;
import org.apache.http.conn.ClientConnectionManager;
import org.apache.http.conn.scheme.PlainSocketFactory;
import org.apache.http.conn.scheme.Scheme;
import org.apache.http.conn.scheme.SchemeRegistry;
import org.apache.http.conn.ssl.SSLSocketFactory;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.impl.conn.SingleClientConnManager;
import android.content.Context;
public class MyHttpClient extends DefaultHttpClient {
final Context context;
public MyHttpClient(Context context) {
this.context = context;
}
@Override
protected ClientConnectionManager createClientConnectionManager() {
SchemeRegistry registry = new SchemeRegistry();
registry.register(new Scheme("http", PlainSocketFactory.getSocketFactory(), 80));
// Register for port 443 our SSLSocketFactory with our keystore
// to the ConnectionManager
registry.register(new Scheme("https", newSslSocketFactory(), 443));
return new SingleClientConnManager(getParams(), registry);
}
private SSLSocketFactory newSslSocketFactory() {
try {
// Get an instance of the Bouncy Castle KeyStore format
KeyStore trusted = KeyStore.getInstance("BKS");
// Get the raw resource, which contains the keystore with
// your trusted certificates (root and any intermediate certs)
InputStream in = context.getResources().openRawResource(R.raw.my_cert);
try {
// Initialize the keystore with the provided trusted certificates
// Also provide the password of the keystore
trusted.load(in, "my_pass".toCharArray());
} finally {
in.close();
}
// Pass the keystore to the SSLSocketFactory. The factory is responsible
// for the verification of the server certificate.
SSLSocketFactory sf = new SSLSocketFactory(trusted);
// Hostname verification from certificate
// http://hc.apache.org/httpcomponents-client-ga/tutorial/html/connmgmt.html#d4e506
sf.setHostnameVerifier(SSLSocketFactory.STRICT_HOSTNAME_VERIFIER);
return sf;
} catch (Exception e) {
throw new AssertionError(e);
}
}
}
И вот код, который его создает:
DefaultHttpClient client = new MyHttpClient(getApplicationContext());
HttpPost post = new HttpPost(server_login_url);
List <NameValuePair> parameters = new ArrayList <NameValuePair>();
parameters.add(new BasicNameValuePair("username", user));
parameters.add(new BasicNameValuePair("password", pass));
try {
post.setEntity(new UrlEncodedFormEntity(parameters, HTTP.UTF_8));
} catch (UnsupportedEncodingException e2) {
// TODO Auto-generated catch block
Log.d(DEBUG_TAG, "in UnsupportedEncodingException - " + e2.getMessage());
e2.printStackTrace();
}
// Execute the GET call and obtain the response
HttpResponse getResponse = null;
try {
getResponse = client.execute(post);
} catch (ClientProtocolException e) {
// TODO Auto-generated catch block
// Toast.makeText(getBaseContext(),message,Toast.LENGTH_LONG).show();
Log.d(DEBUG_TAG, "in ClientProtocolException - " + e.getMessage());
} catch (IOException e) {
// TODO Auto-generated catch block
// Toast.makeText(getBaseContext(),message,Toast.LENGTH_LONG).show();
Log.d(DEBUG_TAG, "in client.execute IOException - " + e.getMessage());
e.printStackTrace();
}
Ошибка перехвачена в блоке IOException. Вот этот стек:
javax.net.ssl.SSLPeerUnverifiedException: No peer certificate
org.apache.harmony.xnet.provider.jsse.SSLSessionImpl.getPeerCertificates(SSLSessionImpl.java:258)
org.apache.http.conn.ssl.AbstractVerifier.verify(AbstractVerifier.java:93)
org.apache.http.conn.ssl.SSLSocketFactory.createSocket(SSLSocketFactory.java:381)
org.apache.http.impl.conn.DefaultClientConnectionOperator.openConnection(DefaultClientConnectionOperator.java:164)
org.apache.http.impl.conn.AbstractPoolEntry.open(AbstractPoolEntry.java:164)
org.apache.http.impl.conn.AbstractPooledConnAdapter.open(AbstractPooledConnAdapter.java:119)
org.apache.http.impl.client.DefaultRequestDirector.execute(DefaultRequestDirector.java:359)
org.apache.http.impl.client.AbstractHttpClient.execute(AbstractHttpClient.java:555)
org.apache.http.impl.client.AbstractHttpClient.execute(AbstractHttpClient.java:487)
org.apache.http.impl.client.AbstractHttpClient.execute(AbstractHttpClient.java:465)
org.ffb.tools.SplashActivity$LoginTask.makeConnection(SplashActivity.java:506)
org.ffb.tools.SplashActivity$LoginTask.doLogin(SplashActivity.java:451)
org.ffb.tools.SplashActivity$LoginTask.doInBackground(SplashActivity.java:439)
org.ffb.tools.SplashActivity$LoginTask.doInBackground(SplashActivity.java:1)
android.os.AsyncTask$2.call(AsyncTask.java:185)
java.util.concurrent.FutureTask$Sync.innerRun(FutureTask.java:306)
java.util.concurrent.FutureTask.run(FutureTask.java:138)
java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1088)
java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:581)
java.lang.Thread.run(Thread.java:1019)
Вот порядок цепочек (из команды openssl):
Цепочка выглядит хорошо, я думаю.
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
2 s:/C=US/O=thawte, Inc./OU=Certification Services Division/OU=(c) 2006 thawte, Inc. - For authorized use only/CN=thawte Primary Root CA
i:/C=ZA/ST=Western Cape/L=Cape Town/O=Thawte Consulting cc/OU=Certification Services Division/CN=Thawte Premium Server CA/emailAddress=premium-server@thawte.com
5 ответов
Этот поток был действительно полезен, когда я отлаживал похожую проблему.
Краткий контрольный список Android 2.3 HTTPS/SSL:
- Если ваш ЦС включен в список доверенных ЦС Android 2.3 , а Thawte - нет необходимости включать сертификат в приложение.
- Android 2.3 не поддерживает указание имени сервера, поэтому, если ваш сервер использует SSL для подтверждения связи, Android может не получить ожидаемые сертификаты.
- У вас установлена цепочка сертификатов на сервере и правильно ли она упорядочена? Большинство браузеров обрабатывают цепочки сертификатов, вышедших из строя, а Android 2.3 - нет. Ответ bdc в вышеупомянутой теме описывает, как проверить действительность вашего SSL-сертификата и цепочку с помощью "openssl s_client -connect yourserver.com:443".
- Когда вы выкапываете старое устройство 2.3, которое у вас есть в нижнем ящике, убедитесь, что его дата и время установлены правильно после долгого отсутствия питания.
У меня была точно такая же проблема, как и у вас. Все работало нормально с android >3.X, но когда я попробовал на некоторых (но не на всех!) Устройствах 2.3.X, я получил то известное исключение "Нет ошибки сертификата пира".
Я много копался в stackru и других блогах, но я не нашел ничего, что работало бы на этих "мошеннических" устройствах (в моем случае: правильное использование хранилища доверенных сертификатов; не требуется sni; правильный порядок цепочки сертификатов на сервере; и т. Д.),
Похоже, что Android Apache HttpClient просто не работал правильно на некоторых устройствах 2.3.X. Исключение "нет сертификатов однорангового узла" возникло слишком рано, чтобы даже достичь пользовательского кода верификатора имени хоста, поэтому подобное решение не сработало для меня.
Вот мой код:
KeyStore trustStore = KeyStore.getInstance("BKS");
InputStream is = this.getAssets().open("discretio.bks");
trustStore.load(is, "discretio".toCharArray());
is.close();
SSLSocketFactory sockfacto = new SSLSocketFactory(trustStore);
sockfacto.setHostnameVerifier(SSLSocketFactory.STRICT_HOSTNAME_VERIFIER);
SchemeRegistry schemeRegistry = new SchemeRegistry();
schemeRegistry.register(new Scheme("http", PlainSocketFactory.getSocketFactory(), 80));
schemeRegistry.register(new Scheme("https", sockfacto, 443));
SingleClientConnManager mgr = new SingleClientConnManager(httpParameters, schemeRegistry);
HttpClient client = new DefaultHttpClient(mgr, httpParameters);
HttpGet request = new HttpGet(url);
HttpResponse response = client.execute(request);
Поэтому я переписал все, используя javax.net.ssl.HttpsURLConnection, и теперь он работает на всех устройствах, которые я тестировал (с 2.3.3 до 4.X).
Вот мой новый код:
KeyStore trustStore = KeyStore.getInstance("BKS");
InputStream is = this.getAssets().open("discretio.bks");
trustStore.load(is, "discretio".toCharArray());
is.close();
TrustManagerFactory tmf = TrustManagerFactory.getInstance("X509");
tmf.init(trustStore);
SSLContext context = SSLContext.getInstance("TLS");
context.init(null, tmf.getTrustManagers(), null);
URL request = new URL(url);
HttpsURLConnection urlConnection = (HttpsURLConnection) request.openConnection();
//ensure that we are using a StrictHostnameVerifier
urlConnection.setHostnameVerifier(new StrictHostnameVerifier());
urlConnection.setSSLSocketFactory(context.getSocketFactory());
urlConnection.setConnectTimeout(15000);
InputStream in = urlConnection.getInputStream();
//I don't want to change my function's return type (laziness) so I'm building an HttpResponse
BasicHttpEntity res = new BasicHttpEntity();
res.setContent(in);
HttpResponse resp = new BasicHttpResponse(HttpVersion.HTTP_1_1, urlConnection.getResponseCode(), "");
resp.setEntity(res);
Цепочка сертификатов и проверка имени хоста работают (я их проверял). Если кто-то хочет лучше взглянуть на изменения, вот разница
Комментарии приветствуются, надеюсь, это поможет некоторым людям.
Другим источником этого сообщения может быть неверная установка даты / времени, например, при использовании устройства, которое несколько месяцев не использовало электричество. Довольно тривиально, но это может быть трудно обнаружить.
Из каких сертификатов вы загружаете R.raw.my_cert
? Эта ошибка говорит либо о неправильной настройке сервера - об отсутствии установки первичных и вторичных промежуточных ЦС Thawte, либо о том, что вы не загружаете и не доверяете правильной цепочке сертификатов.
Логика проверки сертификата (или, точнее, построения цепочки) в (как минимум) Android 2.3 неисправна.
Вот что я заметил:
Если сервер TLS в своей цепочке сертификатов предоставляет только сертификат сервера (неподписанный или самоподписанный), вы можете поместить сертификат сервера в хранилище ключей, и проверка пройдет успешно.
Если сервер TLS в своей цепочке сертификатов также предоставляет промежуточный сертификат CA, то в хранилище ключей вы должны поместить только сертификат корневого CA и убедиться, что хранилище ключей НЕ содержит сертификаты сервера и промежуточного CA (в противном случае проверка может произойти случайно).
Если сервер TLS в своей цепочке сертификатов предоставляет промежуточные и корневые сертификаты CA в правильном порядке, то вам просто нужно убедиться, что сертификат корневого CA находится в хранилище ключей (не имеет значения, имеются ли сертификаты сервера и промежуточного CA).
Таким образом, "правильный / надежный" способ решения этой проблемы состоит в том, чтобы включать в хранилище ключей только сертификаты корневого CA и обвинять конфигурацию сервера в "Нет сертификата однорангового узла" - в случае, если цепочка сертификатов сервера не предоставляет промежуточные сертификаты CA или сертификаты находятся в неправильном порядке, Вы можете протестировать сервер, используя https://www.ssllabs.com/ssltest/.