We're Hiring!

Hands on Mobile API Security: Pinning Client Connections

Globe with connections, city lights and the Sun

Add TLS and Certificate Pinning While Removing Client Secrets

The Hands On Mobile API Security: Get Rid of Client Secrets tutorial demonstrates how to improve API security by removing vulnerable API secrets from mobile apps. In the tutorial, you work with a simple photo client which requires an API key to access NASA’s picture of the day service. An API Proxy, introduced between your client and the picture service, removes the need for storing and protecting the API key on the client itself.

Astropix screenshots

Since I expected most people to run the tutorial with the Android client in an emulator and the proxy server on localhost, I deliberately ran plain HTTP protocol between client and proxy. Though this simplified the tutorial, it’s not appropriate for a production environment. To enhance security, you would want to run HTTPS protocol with certificate pinning to protect against man in the Middle (MitM) attacks.

So in this sequel, you will generate a self-signed certificate during configuration and will modify the Android client to only accept connection requests from a server holding the certificate’s private key.

TLS and Pinning

Transport Layer Security (TLS) establishes a secure connection between client and server offering both privacy and data integrity. Public Key Infrastructure (PKI) is used to establish trust between client and server and establish secure communications.

However, if an attacker can insert himself between client and server, he can intercept the initial TLS handshake, present his own valid PKI certificate, and establish a man in the middle interception point where he can read and alter communication between client and server.

Certificate pinning establishes a white list of certificates that a client will accept. Even though a MitM attack might present a legal certificate, the client will only accept a connection that is signed by a known certificate.

Getting Started

To get started, download the updated Hands On API Proxy source code. In a terminal or command window, change to a directory where you will store the tutorial, and clone this public git repository:

 tutorials$ git clone https://github.com/approov/hands-on-api-proxy.git

Screenshot showing folders

Follow the additional set up instructions from the original tutorial.

The steps directory contains working versions of the proxy and server code at each step of the tutorial, including working versions of the final pinned client and proxy projects.

When you run the configuration setup, it will generate a self-signed digital certificate and corresponding private key, and it will place them in the appropriate pinned client and proxy project locations.

Alternatively, you may use an existing certificate-private key pair, or you could generate your own self-signed certificate-private key pair using OpenSSL:

 $ openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem
        -nodes -days 365

If you completed the previous tutorial, your pen directory should contain working client and proxy projects. If you are starting from a fresh repository, you should start with working copies of the secure client and enhanced proxy. Copy the secure client and enhanced proxy steps into your corresponding playpen directories, for example:

api-proxy$ rm -rf pen/client
api-proxy$ cp -r steps/client/android/2_secure-client pen/client
api-proxy$ rm -rf pen/proxy
api-proxy$ cp -r steps/proxy/node/3_enhanced-proxy pen/proxy

You can build the current pen projects and confirm that the client and proxy are working as expected.

Adding HTTPS to the Localhost Proxy

To start, ensure your digital certificate and private key pair are located in the proxy’s source directory. If you are using the certificates which were generated during configuration, they are named cert.pem and key.pem respectively:

api-proxy$ cp steps/proxy/node/4_pinned-proxy/src/cert.pem
    pen/proxy/src/
api-proxy$ cp steps/proxy/node/4_pinned-proxy/src/key.pem
    pen/proxy/src/

In the proxy pen, ensure that the required modules are installed:

api-proxy$ cd pen/proxy
api-proxy/pen/proxy$ npm install
api-proxy/pen/proxy$ npm install https --save

Next, modify the node proxy to run HTTPS. Require the https package, and replace the default HTTP server’s app.listen() call with a call to create an

HTTPS server using the key pair. The modifications look like this:

Start the proxy server:

api-proxy/pen/proxy$ npm start

Try out your proxy using a browser to call the proxy. Assuming you are running locally on port 8080, replace a direct call to NASA with the now proxied call htttps://localhost:8080/api.nasa.gov/planetary/apod?date=2017–01–01. Using chrome, I see this response:

Screenshot showing Chrome refusing connection

Chrome does not rust self-signed certificates, so it warns us we are trying to run HTTPS with an untrusted certificate, If we proceed anyway using ADVANCED, we should see the correct response from the NASA server through the localhost API proxy:

Screenshot showing accepted connection in Chrome

If you still do not see a valid JSON response, double check your proxy URL, network connectivity, and digital certificate. Triple check that the proxy URL specifies HTTPS protocol. Also in src/config.js, check and force the approov_enforcement value to false. Restart the proxy server, and you will see failing attestations in the proxy log, but the service will not be blocked.

If you are using a certificate signed by a recognized certificate authority (CA), browsers will use the CA’s public key to verify the signature. Since the browser trusts the CA, if the signature is valid then the browser will in turn trust the certificate.

In contrast, self-signed certificates are easy and free to generate, but they are not trusted by most browsers or network client stacks. With a self-signed certificate, we must establish trust another way, typically by verifying that we received the expected digital certificate and that we recognize the host who sent it.

To pin a connection in a production environment, you would likely use a certificate signed by a trusted CA, and that is actually the easier approach to implement. Using a self-signed certificate to pin will require a bit more work inside the client which we’ll tackle next.

For reference, a completed version of the API proxy at this stage is in steps/proxy/android/4_pinned-proxy.

Android Client Pinning

In the Android client app, we want to pin the HTTPS channel to only accept a connection holding our digital certificate.

Start by changing the api_url in config.xml to use HTTPS instead of HTTP:

  <resources>
  <string name="api_url">https://10.0.2.2:8080/api.nasa.gov</string>
  </resources>
view rawconfig.xml hosted with ❤ by GitHub

For networking, the Android client uses the OKHttp library. If our digital certificate is signed by a CA recognized by Android, the default trust manager can be used to validate the certificate. To pin the connection it is enough to add the host name and a hash of the certificate’s public key to the client builder(). See this OKHttp recipe for an example. All certificates with the same host name and public key will match the hash, so techniques such as certificate rotation can be employed without requiring client updates. Multiple host name - public key tuples can also be added to the client builder().

For the tutorial, we are using self-signed rather than CA-signed certificates. To establish trust with a self-signed certificate, you must create a custom TrustManager and also provide a method to verify the target host. OKHttp provides a custom TrustManager recipe.

In the pen client project, begin by adding the self-signed certificate cert.pem into the app’s main assets directory. A copy of cert.pem can be found in steps/client/android/4_pinned-client/app/src/main/assets.

In the App.java file, create a private SSLContextPinner class inside the App class which reads your digital certificate into a KeyStore which in turn initializes a new TrustManagerchain. The TrustManager chain then initializes the TLS SSLContext. Add methods to return the SSLContext and initial X509Trustmanagerfrom the chain:

  /**
  * Creates an SSL context useful for pinning certificates.
  */
  private class SSLContextPinner {
  private SSLContext sslContext;
  private TrustManager trustManager;
   
  public SSLContextPinner(String pemAssetName) {
  try {
  KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
  keyStore.load(null, null);
  InputStream certInputStream = getAssets().open(pemAssetName);
  BufferedInputStream bis = new BufferedInputStream(certInputStream);
  CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
  int idx = -1;
  while (bis.available() > 0) {
  Certificate cert = certificateFactory.generateCertificate(bis);
  keyStore.setCertificateEntry("" + ++idx, cert);
  Log.i("App", "pinned " + idx + ": " + ((X509Certificate) cert).getSubjectDN());
  }
  TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
  trustManagerFactory.init(keyStore);
  TrustManager[] trustManagers = trustManagerFactory.getTrustManagers();
  trustManager = trustManagers[0];
  sslContext = SSLContext.getInstance("TLS");
  sslContext.init(null, trustManagers, null);
  } catch(Exception e) {
  sslContext = null;
  trustManager = null;
  Log.e("App", e.toString());
  }
  }
   
  public SSLContext getSSLContext() { return sslContext; }
   
  public X509TrustManager getX509TrustManager() { return (X509TrustManager) trustManager; }
  };
view rawapp-pinner.java hosted with ❤ by GitHub

Next you must provide a host name verifier. Normally, you could compare the DNS name to the host name associated with the certificate, but with absolute IP addresses, this technique breaks down in browsers and other network stacks. Since we are pinning a known certificate in this example, we will blindly accept that the host name is valid for this tutorial. This is fine as long as no one has stolen our certificate’s matching private key, which is an acceptable risk for this tutorial; however, in production you would use a stronger check. For now, create an always true host name verifier inside the App class:

  /**
  * Passes any host name verification.
  */
  private class NoHostnameVerifier implements HostnameVerifier {
  @Override
  public boolean verify(final String hostname, final SSLSession session) {
  return true;
  }
  };
view rawapp-verifier.java hosted with ❤ by GitHub

Use these classes for certificate pinning and host name checking to build a pinned client for your self-signed certificate. Modify the App class onCreate() method:

  public class App extends Application {
   
  // ...
   
  @Override
  public void onCreate (){
  super.onCreate();
   
  mPlatformSpecifics = new AndroidPlatformSpecifics(this);
  mAttestation = new ApproovAttestation(mPlatformSpecifics);
   
  try {
  SSLContextPinner pinner = new SSLContextPinner("cert.pem");
  mClient = new OkHttpClient.Builder()
  .sslSocketFactory(pinner.getSSLContext().getSocketFactory(), pinner.getX509TrustManager())
  .hostnameVerifier(new NoHostnameVerifier())
  .addInterceptor(new ApproovInterceptor(mAttestation))
  .build();
  } catch (Exception e) {
  Log.e("App", e.toString());
  Log.e("App", "Failed to pin connection");
  throw new IllegalStateException("Failed to pin connection:");
  }
   
  mDownloader = new Picasso.Builder(this)
  .downloader(new OkHttp3Downloader(mClient))
  .build();
  }
  }
view rawapp-client.java hosted with ❤ by GitHub

With those changes, you are ready to test the pinned connection between client and server. Ensure that the proxy server is running, Build and run the modified client app on the Android emulator or device as before. You should see a gallery of current NASA photos in the emulator and photo requests in the API proxy console log.

If you do not see photos, double check your proxy URL, network connectivity, and digital certificate. Double check the proxy IP address (normally10.0.2.2 from the Android emulator) and port address (normally set at 8080). In src/config.js, check and force the approov_enforcement value to false. Restart both proxy server and client app. On the proxy, you will see failing attestations, but the service will not be blocked.

If you want to enforce client attestation checks, you must register the modified client app with the Approov demo service as you did in the original tutorial.

For reference, a completed version of the API proxy at this stage is in steps/proxy/android/4_pinned-proxy.

Not Another Secret?

Congratulations on successfully pinning a self-signed certificate. At this point, you might be asking yourself “if the purpose of the tutorial was to remove secrets from the client app, didn’t we just add a new secret to the app?”

Well, we did add a constant into the app, which was the certificate itself if using self-signing. However, the certificate is the public side of the public-private key pair. The API proxy server will give out this certificate to anyone who tries to establish a secure connection. It’s not a secret. The private key, held on the server and not on the client, is the true secret information.

What is critical is to prevent anyone from adding or replacing your certificate with their own certificate and repackaging the app. Anyone using the modified app would then be vulnerable to a MitM attack. This is where client attestation such as Approov is crucial. Any attempt to tamper with the app will result in an attestation failure.

In the Wild

If you haven’t done so already, I recommend ensuring the Approov demo package is current and reregistering your final pinned client-proxy pair by following the final steps of the original tutorial. This greatly strengthens your security.

Using HTTPS with pinning provides both privacy and data integrity. Pinning is an important technique to prevent man in the middle attacks, but it can be easily compromised if the client can be tampered with. In this case, it’s not a matter of hiding a secret but of ensuring that a public certificate cannot be modified in app. Using a client attestation service effectively removes this attack vector, restoring privacy and integrity to the connection.

 

Try Approov For Free!

Skip Hovsmith

- Senior Consultant at Approov
Developer and Evangelist - Software Performance and API Security - Linux and Android Client and Microservice Platforms