Android supports the use of SSL with standard Android API components, such as HTTPClient
and URLConnection
. However, if you attempt to connect to a secure HTTPS server URL, you may encounter an SSLHandshakeException
. The common issues are:
If the server isn't configured with intermediary certificates, it's simply a case of installing them to allow the connection code to validate the root of trust. However, if the server is using a self-signed certification or a CA-issued certificate but the CA isn't trusted by Android, we need to customize the SSL validation.
A common practice is to develop and test with servers that have self-signed SSL certificates and only use paid CA-signed certificates in the live environment. Therefore, this recipe specifically focuses on robustly validating self-signed SSL certificates.
For this recipe, we will be importing the self-signed SSL certificate into the app, and to do this, we are going to run some terminal commands. This section will cover the tools and commands to download the SSL certificate files on your machine.
The latest version of the Bouncy Castle library is needed later in this recipe to create and import certificates into the truststore. We use Bouncy Castle as it is a robust open source cryptology library that Android has built-in support for. You'll find the bcprov.jar
file at http://www.bouncycastle.org/latest_releases.html. Download and save it to the current working directory. For this recipe, we have saved it to a local directory called libs
so the path to reference the .jar
file is /libs/bcprov-jdk15on-149.jar
(which is the latest version at the time of writing this book).
We will need a self-signed SSL certificate file from the server; if you created yours manually or already have it, you can skip the rest of this section and move on to the recipe.
To create or download an SSL certificate, we will need to take advantage of the open source SSL toolkit known as OpenSSL:
apt-get install openssl
.To get the certificates from the server in the terminal window, type the following command, where server.domain
is either the IP address or server name:
Openssl s_client -showcerts -connect server.domain:443 </dev/null.
The certificate details will be displayed in the console output. Copy and paste the certificate that is defined, starting with -----BEGIN CERTIFICATE-----
and ending with -----END CERTIFICATE-----
, into a new file and save it as mycert.crt
. It's important not to include any additional white space or trailing spaces.
The following screenshot shows an example of the
Openssl –showcerts
command for android.com
:
If you don't have a server yet and want to create a new self-signed certificate to use, we first need to generate a private RSA key using the OpenSSL toolkit. Type the following into a terminal window:
openssl genrsa –out my_private_key.pem 2048
This creates the private key file my_private_key.pem
. The next step is to generate the certificate file using the private key generated in the previous step. In the terminal, type:
openssl req -new -x509 -key my_private_key.pem -out mycert.crt -days 365
Follow the onscreen prompts and fill in the certificate details. Note the common name is typically your server IP address or domain name.
That's it for getting ready! We should have a certificate file in hand for the next section.
Let's get started!
-----BEGIN CERTIFICATE----- WgAwIBAgIDA1MHMA0GCSqGSIb3DQEBBQUAMDwxCzAJBgNVBAYTAlVTMRcwFQYDVQQK … -----END CERTIFICATE-----
For this recipe, we will use the example named mycert.crt
.
.keystore
file that we will refer to as our app's truststore.CLASSPATH
variable so that the following command can access the bcprov.jar
file:$export CLASSPATH=libs/bcprov-jdk15on-149.jar
The preceding command path of the bcprov-jdk15on-149.jar
file should match the -providerpath
argument.
keytool
command:$ keytool -import -v -trustcacerts -alias 0 / -file <(openssl x509 -in mycert.crt) / -keystore customtruststore.bks / -storetype BKS / -providerclass org.bouncycastle.jce.provider.BouncyCastleProvider / -providerpath libs/bcprov-jdk15on-149.jar -storepass androidcookbook
yes
:Trust this certificate? [no]: yes
The output file is customtruststore.bks
, with the public certificate added. The truststore is protected with a password, androidcookbook
, which we will reference in the code when we load the truststore in the app. We set the –storetype
argument as BKS
, which denotes the Bouncy Castle Keystore type, also explaining the .bks
extension. It's possible to import multiple certificates into your truststore; for example, development and test servers.
Difference between keystore and truststore
Although they are the same type of file (.keystore
), and in fact can be the same file, we tend to have separate files. We use the term truststore
to define a set of third-party public certificates you expect to communicate with. Whereas, a keystore is for private keys and should be stored in a protected location (that is, not in the app).
raw
folder of your Android app; if the folder doesn't exist, create it:
/res/raw/customtruststore.bks
raw
directory into a KeyStore
object:private static final String STORE_PASSWORD = "androidcookbook"; private KeyStore loadKeyStore() throws Exception { final KeyStore keyStore = KeyStore.getInstance("BKS"); final InputStream inputStream = context.getResources().openRawResource( R.raw.customtruststore); try { keyStore.load(inputStream, STORE_PASSWORD.toCharArray()); return keyStore; } finally { inputStream.close(); } }
Here, we create an instance of the KeyStore
class with the type BKS
(Bouncy Castle Keystore) that matches the type we created. Conveniently, there is a .load()
method, which takes the input stream (InputStream
) of the loaded .bks
file. You'll notice we are using the same password we used to create the truststore to open, verify, and read the contents. The primary use of the password is to verify the integrity of the truststore rather than enforce security. Especially since the truststore contains the server's public certificate, it is not a security issue having this hardcoded, as the certificates are easily accessible from the URL. However, to make things harder for attackers, it could be a good candidate for DexGuard's string encryption as mentioned in Chapter 5, Protecting Applications.
DefaultHttpClient
to use the local truststore:public class LocalTrustStoreMyHttpClient extends DefaultHttpClient { @Override protected ClientConnectionManager createClientConnectionManager() { SchemeRegistry registry = new SchemeRegistry(); registry.register(new Scheme("http", PlainSocketFactory .getSocketFactory(), 80)); try { registry.register(new Scheme("https", new SSLSocketFactory( loadKeyStore()), 443)); } catch (Exception e) { e.printStackTrace(); } return new SingleClientConnManager(getParams(), registry); } }
We override the createClientConnectionManager
method so that we can register a new SSLSocketFactory
interface with our local truststore. For brevity of the code samples, here we have caught the exception and printed the error to the system log; however, it is recommended to implement appropriate error handling and reduce the amount of information logged when using this in live code.
GET
request using HttpClient
:public HttpResponse httpClientRequestUsingLocalKeystore(Stringurl) throws ClientProtocolException, IOException { HttpClient httpClient = new MyHttpClient(); HttpGet httpGet = new HttpGet(url); HttpResponse response = httpClient.execute(httpGet); return response; }
This shows us how to construct a simple HTTP GET
request and use the LocalTrustStoreMyHttpClient
class, which doesn't throw SSLHandshakeException
because the self-signed certificate from the server can be successfully verified.
That concludes this recipe; we can communicate with Internet resources that are protected by SSL and signed with our self-signed SSL certificate.
In general, when dealing with SSL, a common mistake is to catch and hide certificate and security exceptions. This is exactly what an attacker is relying on to dupe an unsuspecting app user. What you choose to do about SSL errors is subjective and depends on the app. However, blocking networking communications is usually a good step to ensure that data is not transmitted over a potentially compromised channel.
It is common for Android application developers to know at compile/build time the servers they are commutating with. They may even have control over them. If you follow the validation steps noted here, there's no security issue with using self-signed certificates in a live environment. The advantage is that you'll insulate yourself from certificate authority compromise and save money of SSL certificate renewal fees.
There's no additional security benefit, but you may prefer using the HttpsURLConnection
API. For this, we take a slightly different approach and create a custom TrustManager
class, which verifiers our local truststore file:
TrustManager
class:public class LocalTrustStoreTrustManager implements X509TrustManager { private X509TrustManager mTrustManager; public LocalTrustStoreTrustManager(KeyStore localTrustStore) { try { TrustManagerFactory factory = TrustManagerFactory .getInstance(TrustManagerFactory.getDefaultAlgorithm()); factory.init(localTrustStore); mTrustManager = findX509TrustManager(factory); if (mTrustManager == null) { throw new IllegalStateException( "Couldn't find X509TrustManager"); } } catch (GeneralSecurityException e) { throw new RuntimeException(e); } } @Override public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { mTrustManager.checkClientTrusted(chain, authType); } @Override public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { mTrustManager.checkServerTrusted(chain, authType); } @Override public X509Certificate[] getAcceptedIssuers() { return mTrustManager.getAcceptedIssuers(); } private X509TrustManager findX509TrustManager(TrustManagerFactory tmf) { TrustManager trustManagers[] = tmf.getTrustManagers(); for (int i = 0; i < trustManagers.length; i++) { if (trustManagers[i] instanceof X509TrustManager) { return (X509TrustManager) trustManagers[i]; } } return null; } }
We implement the X509TrustManager
interface, and the constructor of our LocalTrustStoreTrustManager
class takes a KeyStore
object, which we loaded in a previous step defined earlier in the recipe. As previously noted, this KeyStore
object is referred to as our truststore because it contains the certificate we trust. We initialize the TrustManagerFactory
class with the truststore and then using the findX509TrustManager()
method, we get the system-specific implementation of the X509TrustManager
interface. We then keep a reference to this TrustManager
, which uses our truststore to verify whether a certificate from a connection is trusted, rather than using the system truststore.
GET
request using HttpsURLConnection
and the custom TrustManager
class created in the previous step:public InputStream uRLConnectionRequestLocalTruststore(String targetUrl) throws Exception { URL url = new URL(targetUrl); SSLContext sc = SSLContext.getInstance("TLS"); sc.init(null, new TrustManager[] { new LocalTrustStoreTrustManager( loadKeyStore()) }, new SecureRandom()); HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory()); HttpsURLConnection urlHttpsConnection = (HttpsURLConnection) url.openConnection(); urlHttpsConnection.setRequestMethod("GET"); urlHttpsConnection.connect(); return urlHttpsConnection.getInputStream(); }
We initialize the SSLContext
with the LocalTrustStoreTrustManager
class so that when we call sc.getSocketFactory()
, it will use our TrustManager
implementation. This is set on the HttpsURLConnection
by overriding the default using setDefaultSSLSocketFactory()
. That's all you need to successfully connect to our self-signed SSL resources with URLConnection
.
This is an antipattern that unfortunately is posted on various forums and message boards when developers are trying to work with self-signed certifications or SSL certificates signed by an untrusted certification authority.
Here, we see an insecure implementation of the X509TrustManager
interface:
public class TrustAllX509TrustManager implements X509TrustManager { @Override public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { // do nothing, trust all :( } @Override public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { // do nothing, trust all :( } @Override public X509Certificate[] getAcceptedIssuers() { return null; } }
As you can see from the code, the checkServerTrusted
method has no validation implemented consequently, and all servers are trusted. This leaves HTTPS communications exposed to a man-in-the-middle (MITM) attack, which defeats the whole point of using certificates.
18.226.104.202