'No peer certificate' error in Android 2.3 but NOT in 4

Getting the "javax.net.ssl.SSLPeerUnverifiedException: No peer certificate error" in an emulator running Android 2.3 but NOT in 4. In 4 it works perfectly. I'm trying to connect to a live server via https. It uses a valid Thawte certificate, works fine in all browsers and Android 3 and 4.

If anyone has code help, PLEASE and thanks. Also, if anyone has any suggestions on a secure workaround, I'd appreciate it. I'm still learning, and I've been on this problem for a week. It has to end, so I can continue working and learning. Urgh.

Here is HttpCLient code, courtesy Antoine Hauck (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);
        }
    }
}

And here is the code that instantiates it:

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();
            }

The error is caught in the IOException block. Here is the stack:

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)

Here is the chain order (from openssl command):

The chain looks good I think.

    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/[email protected]

Solution 1:

This thread was really helpful when I debugged a similar issue.

Summary Android 2.3 HTTPS/SSL checklist:

  • If your CA is in Android's 2.3 list of trusted CA's -- and Thawte is -- there's no need to include the certificate in the app.
  • Android 2.3 does not support Server Name Indication so if your server is relying on it for SSL handshaking, Android may not be getting the certificates you're expecting.
  • Do you have certificate chain on the server installed, and is it ordered correctly? Most browsers handle out-of-order certificate chains but Android 2.3 does not. bdc's answer in the thread I mentioned above describes how to check the validity of your SSL certificate and chain with "openssl s_client -connect yourserver.com:443".
  • When digging up that old 2.3 device you have in your bottom drawer, please ensure its date and time are set correctly after being powerless for too long.

Solution 2:

I had exactly the same issue as you. Everything was working fine with android >3.X but when I tried with some (but not all !) 2.3.X devices I got that famous "No peer certificate error" exception.

I dug a lot through stackoverflow and other blogs but I haven't found anything that worked on those "rogue" devices (in my case: correct use of truststore; no sni required; correct cert chain order on server; etc ...).

It's look like that android's Apache HttpClient was just not working correctly on some 2.3.X devices. The "no peer certificates" exception was occurring too early to even reach a custom hostname verifier code, so solution like that one were not working for me.

Here was my code :

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);

So I rewrote everything using javax.net.ssl.HttpsURLConnection and now it's working on all devices I have tested (from 2.3.3 to 4.X).

Here is my new code :

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);

Certificate chain and hostname validation are working (I tested them). If anyone want to take a better look on the change, here is a diff

Comments are welcome, I hope it will help some people.