Friday, February 05, 2010

Android: Trusting SSL certificates

We use a self-signed SSL certificate for the test version of our backend web service. Since our certificate isn't signed by a CA that Android trusts by default, we need to add our server's public certificate to our Android app's trusted store.

These same instructions apply to trusting a custom CA, except you'd get the public certificate directly from the CA instead of from a server.

Required tools:

1. Grab the public certificate from the server you want to trust. Replace ${MY_SERVER} with your server's address.

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

For example, here's the PEM-encoded public certificate from google.com:

-----BEGIN CERTIFICATE-----
MIIDITCCAoqgAwIBAgIQL9+89q6RUm0PmqPfQDQ+mjANBgkqhkiG9w0BAQUFADBM
MQswCQYDVQQGEwJaQTElMCMGA1UEChMcVGhhd3RlIENvbnN1bHRpbmcgKFB0eSkg
THRkLjEWMBQGA1UEAxMNVGhhd3RlIFNHQyBDQTAeFw0wOTEyMTgwMDAwMDBaFw0x
MTEyMTgyMzU5NTlaMGgxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlh
MRYwFAYDVQQHFA1Nb3VudGFpbiBWaWV3MRMwEQYDVQQKFApHb29nbGUgSW5jMRcw
FQYDVQQDFA53d3cuZ29vZ2xlLmNvbTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkC
gYEA6PmGD5D6htffvXImttdEAoN4c9kCKO+IRTn7EOh8rqk41XXGOOsKFQebg+jN
gtXj9xVoRaELGYW84u+E593y17iYwqG7tcFR39SDAqc9BkJb4SLD3muFXxzW2k6L
05vuuWciKh0R73mkszeK9P4Y/bz5RiNQl/Os/CRGK1w7t0UCAwEAAaOB5zCB5DAM
BgNVHRMBAf8EAjAAMDYGA1UdHwQvMC0wK6ApoCeGJWh0dHA6Ly9jcmwudGhhd3Rl
LmNvbS9UaGF3dGVTR0NDQS5jcmwwKAYDVR0lBCEwHwYIKwYBBQUHAwEGCCsGAQUF
BwMCBglghkgBhvhCBAEwcgYIKwYBBQUHAQEEZjBkMCIGCCsGAQUFBzABhhZodHRw
Oi8vb2NzcC50aGF3dGUuY29tMD4GCCsGAQUFBzAChjJodHRwOi8vd3d3LnRoYXd0
ZS5jb20vcmVwb3NpdG9yeS9UaGF3dGVfU0dDX0NBLmNydDANBgkqhkiG9w0BAQUF
AAOBgQCfQ89bxFApsb/isJr/aiEdLRLDLE5a+RLizrmCUi3nHX4adpaQedEkUjh5
u2ONgJd8IyAPkU0Wueru9G2Jysa9zCRo1kNbzipYvzwY4OA8Ys+WAi0oR1A04Se6
z5nRUP8pJcA2NhUzUnC+MY+f6H/nEQyNv4SgQhqAibAxWEEHXw==
-----END CERTIFICATE-----

2. Android has built-in support for the Bouncy Castle keystore format (BKS). Put Bouncy Castle's jar in your classpath, and create a keystore containing only your trusted key.

export CLASSPATH=bcprov-jdk16-145.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 /usr/share/java/bcprov.jar \
      -storepass ez24get

3. Create a custom Apache HttpClient that uses your custom store for HTTPS connections.

import android.content.Context;
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 java.io.InputStream;
import java.security.KeyStore;

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));
    registry.register(new Scheme("https", newSslSocketFactory(), 443));
    return new SingleClientConnManager(getParams(), registry);
  }

  private SSLSocketFactory newSslSocketFactory() {
    try {
      KeyStore trusted = KeyStore.getInstance("BKS");
      InputStream in = context.getResources().openRawResource(R.raw.mystore);
      try {
        trusted.load(in, "ez24get".toCharArray());
      } finally {
        in.close();
      }
      return new SSLSocketFactory(trusted);
    } catch (Exception e) {
      throw new AssertionError(e);
    }
  }
}

That's it! If you think this kind of stuff is fun, Square is hiring.

17 Comments:

Blogger Unknown said...

interface ForTestCallback {
void Finished(String result, String error);
}

public class ForTest extends Thread {
private ForTestCallback callBack;
private Handler handler;
private String url;

public ForTest(String adress, ForTestCallback callBack) {
this.callBack = callBack;
this.url = adress;
handler = new Handler();
}

public void begin() {
start();
}

public void run() {
try {
final long t0 = System.currentTimeMillis();
final String result;

// fill empty keystore received a certificate
KeyStore store = KeyStore.getInstance(KeyStore.getDefaultType());// BKS
store.load(null); // init empty keystore
URLConnection uc = new URL(url).openConnection();
try {
uc.connect();
Certificate certs[] = ((HttpsURLConnection)uc).getServerCertificates();
for (int i=0; i<certs.length; i++)
store.setCertificateEntry("cert_"+i, certs[i]);
}
catch(Exception e) {
Log.e("GET_CERT",e.getMessage());
}
finally {
if (uc!=null && uc instanceof HttpURLConnection)
((HttpsURLConnection)uc).disconnect();
}
// check the "trusted" connection
HttpEntity responseEntity;
HttpGet httpGet = new HttpGet(url);
HttpClient client = new MyHttpClient(store);
httpGet.addHeader("Content-Type", "text/xml");
HttpConnectionParams.setConnectionTimeout(client.getParams(), 15000);
HttpResponse getResponse = client.execute(httpGet);
responseEntity = getResponse.getEntity();
result = EntityUtils.toString(responseEntity);

final long t1 = System.currentTimeMillis();
handler.post(new Runnable() {
public void run() {
callBack.Finished(result+" for " + (t1-t0) + "ms", null);
}
});
}
catch (final Exception e) {
handler.post(new Runnable() {
public void run() {
Log.d("GET_ERROR", "error", e);
callBack.Finished(null, e.getMessage());
}
});
}
}

//Your code, slightly modified ...
public class MyHttpClient extends DefaultHttpClient {
final KeyStore store;
public MyHttpClient(KeyStore store) {
this.store = store;
}
@Override
protected ClientConnectionManager createClientConnectionManager() {
SchemeRegistry registry = new SchemeRegistry();
registry.register(new Scheme("http", PlainSocketFactory.getSocketFactory(), 80));
registry.register(new Scheme("https", newSslSocketFactory(), 443));
return new SingleClientConnManager(getParams(), registry);
}
private SSLSocketFactory newSslSocketFactory() {
try {
return new SSLSocketFactory(store);
} catch (Exception e) {
throw new AssertionError(e);
}
}
}
}
/*
example to call
public void callTest() {
final String url = "https://login.yahoo.com/"; // work fine
// final String url = "https://www.comodo.com/products/comodo-products.php"; //gets error
// final String url = "https://www.google.com/accounts/ManageAccount"; //gets error
ForTest test = new ForTest(url, new ForTestCallback() {
@Override
public void Finished(String result, String error) {
setProgressBarIndeterminateVisibility(false);
// show info...
startBtn.setText(bHttpsEnvSet?"get ready...":"replay...");
// next work...
}
});
setProgressBarIndeterminateVisibility(true);
startBtn.setText("testing...");
test.begin();
}
*/

11:17 AM  
Blogger Unknown said...

Hi!
I tried your code, but it did not solve my problem: (
Here is my code and your little corrected.
As noted, the connection is fine, if the server has only one certificate in the chain, otherwise we get "Not trusted server certificate"
Maybe I am wrong somewhere. I would be very grateful if you help
I deal with problems.
Thank you.

11:19 AM  
Blogger Mory said...

Works great for me. Thx a lot, that saved my day !!!

5:32 AM  
Blogger Unknown said...

Hi Bob,
I am facing the following error in my android application while connecting to https server

"Not a trusted certificate"

When i used your command "echo | openssl s_client -connect ${MY_SERVER_ADDRESS}:443 2>&1 | \ sed -ne '/-BEGIN CERTIFICATE-/,/-END CERTIFICATE-/p' > mycert.pem" to get the certificate from server, no certificate was returned and i am unable to trace where i went wrong.Kindly assist me in getting certificate from server and creating R.raw.mystore file.

10:15 AM  
Blogger Bryan said...

This post is incredibly helpful, thanks.

3:34 PM  
Blogger Jay Khimani said...

Thanks Bob. This works for me in my test environment where we have self signed certificates.

Cheers !!!
- Jay

2:28 AM  
Blogger Vittorio Pavesi said...

I'm experiencing a "NOT TRUSTED CERTIFICATE" error even if the certificate has been signed by a public CA (Thawte) and all browser works properly with it.
Have you got any suggestion ?

1:47 PM  
Blogger Philip Wrenn said...

Worked like a charm for trusting CACert CA, Thanks!

7:47 AM  
Blogger Krishna Chaitanya A said...

Thank you for this blog.

I have a similar situation where I need to generate the certificate on the device itself from my app. Is there a way to do that?
I tried using the Bouncycastle jar[bcprov-jdk16-145.jar] but the problem is that, since android already included those classes, it is not able to resolve these classes from the jar that I add. So I could not use the classes to generate certificates.

Do you know of a way to generate the certificates from my android app itself?

Thanks again.

5:33 PM  
Blogger steff said...

Hi, nice listing. But I've got a weird one. Works flawlessly for all clients over WiFi. However, it fails on some devices over 3G/GPRS with CONNECTION_TIMED_OUT or CONNECTION_REFUSED exception. Any pieces of advice?
Thanks in advance

2:59 PM  
Blogger Ranjit Iyer said...

What version of the Bouncy castle Jar was used in this example? I used the 1.6 version from http://www.bouncycastle.org/latest_releases.html but get this exception:

java.io.IOException: Wrong version of key store.

7:56 PM  
Blogger Jay Khimani said...

Thanks Bob, very helpful

5:22 AM  
Blogger Unknown said...

The bounycastle jar is ~2mb? Am I seeing this right?


http://www.bouncycastle.org/latest_releases.html
bcprov-jdk16-146.jar

8:44 PM  
Blogger Abe Estrada said...

In your code, what does "ez24get" means?
Is that a key that I need to use with the certificate?

I continue getting the "Not trusted server certificate" error.

12:07 PM  
Blogger Bob said...

Abe, that's the password.

12:08 PM  
Blogger John Doe said...

I swear I am doing everything right, but am still getting: "Not trusted server certificate". Could there be something in the way I am executing my GET?

DefaultHttpClient httpclient = new MyHttpClient(getApplicationContext());
HttpGet get = new HttpGet("https://www.coffeecup.com/api/sdrive/user_accounts_list.json");

try {
HttpResponse response = httpclient.execute(get);
} catch (ClientProtocolException e) {
System.out.println(e.getMessage());
} catch (IOException e) {
System.out.println(e.getMessage());
}

12:24 PM  
Blogger Unknown said...

I can't get the codes to work on my machine. Kobtyh, can I have the source code for this? I am really stuck and I got tons of errors, even importing the missing classes didnt work.

3:32 AM  

Post a Comment

<< Home