HOW WE INTEGRATED APPROOV WITH CORDOVA

Friday 15 June 2018 By Johannes Schneiders

Topics: Integration, Cordova

Architecture in Cordoba, Andalusia, Spain

OUTLINE

WHAT IS THIS ABOUT?

In this blog we will see how to integrate Approov with Cordova as an example for how to integrate Approov with a mobile app development platform while keeping changes to said platform and the apps that are built on top of it to a minimum. This is a quite detailed exposition on how to build an Approov plugin to use with Cordova Advanced HTTP that is also relevant for how one would use Approov on other mobile app development platforms. The integration techniques used here can also be applied to other, non-Approov or non-Cordova related integrations. If you would just like to know how to use the Approov plugin head straight to Tying It All Together - Using Approov in a Cordova App, but if you are curious about the details of how we did the integration and how it works, read on.

Cordova is a platform for building native mobile applications using HTML, CSS and JavaScript. It is an open source project managed by the Apache Software Foundation.

Approov improves mobile mobile security by enabling dynamic software attestation for mobile apps. It allows mobile apps to uniquely authenticate themselves as the genuine, untampered software you originally published. Upon successfully passing the integrity check the app is issued a short-lifetime token which can then be presented to your API with each request. This allows your server side implementation to differentiate between requests from known apps, which will contain a valid token, and requests from other sources, which will not. This gives you complete control over what you allow to talk to your mobile API server. Watch the Approov Product Video for a quick, three minute overview.

Cordova Advanced HTTP (also on NPM) is a popular Cordova plugin for communicating with HTTP(S) servers and works for both Android and iOS. Throughout this blog we are using Cordova Advanced HTTP as the example for Approov integration.

APPROOV IN A NUTSHELL

Before we look at the actual integration with a platform, we need to know a little more about how Approov works and how you use it with a native app. Approov is provided as a native SDK (Android .aar, iOS .framework) that you integrate into your Android and iOS apps (see also the Approov SDK User Guides for Android and iOS).

The Approov SDK provides a simple API for authenticating a mobile app and requesting an Approov token from the Approov Cloud Service to represent the app’s authenticity. The app can then transmit the token to your mobile API servers for validation:
The SDK initiates the token fetch to which the Approov Service replies with a challenge to the SDK to prove its own and the app's authenticity. The SDK responds to the challenge by performing an integrity check of itself and an authenticity check of the app, the result of which is verified remotely by the Approov Cloud Service. The Approov Cloud Service then provides the SDK with a unique, cryptographically-signed, time-limited Approov token whose validity is only known by the Approov Cloud Service and the API servers which hold the “token secret”.

While the Approov token representing the app’s authenticity is securely delivered to the SDK, and by extension the app, it is the mobile app's responsibility to ensure the token is securely transmitted to the mobile API server. The connection between app and API must use HTTPS and be pinned to the API server to prevent theft of valid Approov tokens through a man-in-the-middle (MITM) attack, because a stolen, valid Approov token can be used to impersonate a bona-fide app.

This can be achieved either by static certificate pinning or through Approov's dynamic certificate pinning capability.

Static pinning techniques specify the certificate you are expecting from the API server and ensure that secure connections are only made to that server, thus blocking MITM attempts.
Static pinning can be difficult to manage operationally due to dependencies between app and server. The app needs to know the certificate to pin and this is usually embedded in the app. This means that a certificate update on the API server requires that the app must also be updated.

Approov's dynamic certificate pinning scheme detects whether the HTTPS connection between the mobile app and the API server has been intercepted by a MITM attack without statically defining the certificate in the app. It prevents the theft and reuse of valid Approov tokens and ensures that any Approov token transmitted over an insecure connection is an invalid one. It also allows you to check on the API server whether the connection is secure. It adapts dynamically to legitimate certificate changes on the API server and dynamically recovers once an attempt to MITM the connection ends. MITM detection is enabled by simply specifying the hostname of the API server you are about to access as a parameter to the corresponding call to the Approov SDK's fetchApproovToken() function. Note that the privacy of data, other than valid Approov tokens, transmitted from the mobile app to the API server is not guaranteed by this scheme (it's on the roadmap).

APPROOV DYNAMIC PINNING DETAILS

Approov's dynamic certificate pinning for protection against token theft through MITM works in a slightly different way from static pinning.
Firstly it is important to realize that only the propagation of valid Approov tokens is prevented. Invalid Approov tokens (and the requests to which they are attached) are allowed to be sent.
Secondly we should remember the app doesn't know whether an Approov token is valid, only Approov Service and API server do.

In Approov dynamic pinning, we detect in the app whether the certificate on the connection to the API server has changed.
But we determine in the Approov Cloud Service whether the certificate on the connection from the app to the API server is correct, i.e. whether it matches the one you serve from the mobile API server. If there is no match, the Approov Cloud Service issues an invalid Approov token.

SO HOW DOES IT WORK?

When the app calls the Approov SDK's fetchApproovToken() method with an URL parameter, the SDK fetches the HTTPS certificate from the URL's hostname and sends it attached to the token request to the Approov Cloud Service. The Cloud Service then also fetches the HTTPS certificate from the URL's hostname. If the certificates match, and the app has passed the authenticity challenge, the Approov Cloud Service issues a valid Approov token. If the certificates do not match, the Approov Cloud Service issues an invalid Approov token.

  • If the issued Approov token is valid and the certificate on the connection to the API server has not changed since the Approov SDK last retrieved the certificate and requested a token, then the connection to the API server is secure. The request to the API server is sent and includes a valid token, which can be verified on the API server and the appropriate response sent to the app.

  • If the connection is insecure and the certificate on the connection to the API server has not changed since the Approov SDK last retrieved it and requested a token, then the issued Approov token is not valid. The request to the API server is sent but includes an invalid token, which can be detected by the API server and the appropriate action can be taken.

  • If the certificate on the connection to the API server has changed since the Approov SDK last retrieved it and requested a token, the status of the connection is unknown. It must be ensured that the request is not sent and the certificate validation in the Approov Cloud Service must be repeated. This is ensured by using getCert(URL), which returns the certificate for the URL's hostname as retrieved and cached by the Approov SDK, and then pinning this cert. This causes any request to fail (i.e. not being sent) where the cert has changed from the one pinned. If this occurs we clear out the SDK's certificate cache using clearCerts()and re-start the authentication and certificate retrieval process by calling fetchApproovToken(URL).

For a very detailed explanation and example please refer to the Approov MITM Detection documentation. or the Detect MITM on the Server Side: Advanced API Protection video.

APPROOV SDK

The native Approov SDK provides a lean interface that consists of just a handful of functions.

INITIALIZATION

  • initialize(ApproovConfig)
    Before using any features of Approov you will first need to initialize the Approov SDK. Initialization should be performed at application start-up so the Approov SDK is available to perform attestations straight away.

APPROOV TOKEN FUNCTIONALITY

  • ApproovTokenFetchResult fetchApproovTokenAndWait(URL)
    fetchApproovToken(ApproovTokenFetchCallback, URL)
    Synchronously or asynchronously perform an attestation of the running app and fetch an Approov token from the Approov Service. The synchronous version blocks the caller until completion (or timeout) and returns the result of the token fetch operation. The asynchonous version returns immediately and on completion (or time out) invokes the callback with the result of the token fetch operation.
    If the URL parameter is non-null, the fetchApproovToken method extracts the host name from the URL parameter and both the Approov SDK and the Approov Cloud Service retrieve the HTTPS certificate from the host. If the certificates do not match, the Approov token issued will not pass validation by the API server.
    If the Approov SDK cannot retrieve a valid TLS certificate for the host, a failure status will be returned and no token will be available. Usage:

    • Immediately before making an HTTPS request to your server/cloud/backend API, make a fetchApproovToken call to retrieve a token from the Approov Service.
    • Add the Approov token to the header of the request and send the request securely, using HTTPS with certificate pinning, to the API server.
    • Your server can then check the token's validity and, if the token is found to be invalid, which indicates that the request did not come from a genuine app or was transmitted through an insecure connection, reject the request.
  • setTokenPayloadValue(data)
    Include a hash of arbitrary data as one of the Approov token’s claims. This is intended for long lived data, such as an OAuth token or a user session id, that can be used to uniquely identify a user and thus to extend the security of the Approov token through a strength in depth approach by tying a particular Approov token to an arbitrary string.
    The data is supplied to the Approov SDK as an ASCII encoded string. The Approov SDK puts the string's bytes through a SHA-256 hash and then base64-encodes the result. The Approov Cloud Service includes this in the Approov token which makes it available to the API server.

DYNAMIC CERTIFICATE PINNING FUNCTIONALITY FOR MITM TOKEN THEFT PROTECTION:

  • getCert() Returns the DER binary format data of the X.509 TLS leaf certificate for the host name contained in the given URL as retrieved and cached by the Approov SDK. Note that fetchApproovToken with the URL containing the host name must have been called prior to invoking this method, or the certificate cache will not hold the requested certificate. The certificate will also not be present in the SDK's cache if the certificate could not be retrieved or if the prior call to fetchApproovToken(URL) failed for any reason. Usage:

    • After making the fetchApproovToken(URL) call and adding the token to the request to the API server, but before actually sending the request, call getCert(URL).
    • Pin the returned cert so it gets checked before the request is sent.
    • If the request fails because of the pinning, call clearCerts() and go back and retry the request from the step where fetchApproovToken(URL) is called.
    • This will retrieve the certificate again and fetch a new Approov token from the Approov Cloud Service and allow recovery from certificate change caused either by a genuine certificate update on the API server or by an MITM attack.
  • clearCerts() Clears the SDK's internal cache of X.509 TLS leaf certificates. Subsequent calls to fetchApproovToken(URL) will result in a new certificate being fetched for the host specified by the URL. Calls to getCert(URL) will return null until a corresponding fetchApproovToken(URL) call has been made.
    Usage:
    clearCerts() should be called if you suspect that the certificate information stored is incorrect, for example because there is a mismatch between the certificate returned by getCert() and your connection's certificate.

CORDOVA ADVANCED HTTP

The Cordova Advanced HTTP plugin provides functions for communicating with HTTP(S) servers for both Android and iOS. Its Android implementation is based on Http Request a convenience library for using an HttpURLConnection to make requests and retrieve the response. Its iOS implementation uses AFNetworking a networking library built on top of Apple's Foundation framework's URL Loading System.

Cordova Advanced HTTP supports the corresponding HTTP(S) requests through the JavaScript functions post, get, put, patch, delete and head, and also provides functionality for uploading and downloading files through its uploadFile and downloadFilefunctions. All these functions take an URL, some data or query parameters, headers and success and error callbacks as their arguments and on completion, dependent on outcome, call the success or error function with the response as its argument.

Example: The function post(url, data, headers, success, failure) takes five arguments and calls the appropriate response function:

  • url: URL

  • data: payload to be sent to the server

  • headers: headers object (key value pair), will be merged with global values

  • success: success function called on success with a response object that has three properties: status, data and headers:

    • status: HTTP response code as numeric value
    • data: response from the server as a string
    • headers: object containing the headers where the keys are the header names (in lowercase) and the values are the respective header values
  • failure: error function called on failure with a response object that has three properties: status, error and headers:

    • status: HTTP response code as numeric value
    • error: error response from the server as a string
    • headers: object containing the headers

MAKING IT WORK - ADDING APPROOV MOBILE API PROTECTION TO A PLATFORM

We want to provide a ready solution for using Approov with Cordova and are basing this on the Cordova Advanced HTTP plugin. But we also want to give a more general guide on how to integrate Approov into Cordova apps that use a different plugin for their HTTP(S) communication and provide a blueprint for how to integrate Approov with app development platforms in general.

Our overriding goals are that the details of using Approov should stay hidden as much as possible and that the effort for anyone to use Approov in their Cordova apps, be they existing or future ones, be kept to a minimum. We ended up with these:

  • Thou shall not change the interface of any existing Cordova plugin.
  • Thou shall not mix Approov-specific code with existing plugin code, or indeed app code.
  • Thou shall require no more than one change to an existing Cordova app.

This leads directly to our integration approach:

INTEGRATION APPROACH

We are making no changes to the JavaScript interface of the existing HTTP(S) plugin that we are basing our integration on - that means there are no changes in the way existing apps use the HTTP(S) plugin, examples continue to work, etc. But we are extending the plugin's implementation by adding general-purpose interceptor hooks to the existing HTTP(S) plugin so additional functionality can be called if required. As long as these hooks are not used, the plugin's functionality and behaviour are not affected. Our aim is for these hooks to be general and useful enough that they will be accepted as contributions by the maintainers of the existing HTTP(S) plugin. Failing that, since we are only adding small amounts of code to the plugin, it should be easy to keep our version up to date with respect to the original.
In Cordova Advanced HTTP we add a hook just before the call that sends an HTTP(S) request and, for iOS only, another hook in the code that handles a request failure. These hooks can be used by anyone who wants to apply "last-minute" changes to a request or do some special request-failure-handling.

We use these hooks to trigger the execution of interceptors that implement the necessary Approov functionality. Because the calls to the native Approov SDK are performed by the interceptors, an existing mobile app that uses the Cordova Advanced HTTP plugin does not require any code change with regards to its use of the plugin.
We provide the interceptor implementation as a separate Cordova plugin called Cordova Approov HTTP (using the naming style of Cordova Advanced HTTP). The plugin implements all necessary Approov functionality in native code, not JavaScript, effectively hiding the details of the Approov attestation scheme from the app. This is for ease of use and security, but also because access to request headers, connections, certificates, etc is usually not available at the JavaScript level.
The Cordova Approov Plugin provides all necessary functionality for using the Approov scheme (fetching an Approov token, adding it to a HTTPS request, setting up dynamic SSL pinning before each request, if so configured, handling a failure to establish a TLS connection) and performs all the necessary calls to the native Approov SDK.

What is left, is to provide a way to configure whose host/domain's communication should be protected by Approov and to configure the Approov native SDK itself: The interface of Cordova Approov HTTP consists of just a single configuration function. Typically, this function will be called once at the start of the app, the rest is automatic. This "Configure and Forget" approach results in the only required change to an app being the import of the Approov plugin and the call to its configuration function. This makes for a pretty flat learning curve for anyone who is already using the Cordova Advanced HTTP plugin.

One final thing to be aware of is that requests that always succeeded before Approov protection with dynamic certificate pinning was enabled, may now fail because of a MITM attack or a genuine certificate update - which is of course the purpose of dynamic pinning. To recover from this when the attack ends or to start using a new genuine certificate, such requests must be re-tried - something many mobile apps will do for failed requests anyway, but if an app does not, this would be a further necessary change.

Looking forward, the approach outlined here is the blueprint for Approov integration with other platforms and plugins: Implement the hooks and re-use the Approov-specific code shown here.

On to the actual implementation:

CORDOVA ADVANCED HTTP - ADDING INTERCEPTOR HOOKS

Here is one we made earlier: the complete source code of Cordova Advanced HTTP with interceptor hooks is on GitHub.

ANDROID

On Android we add a hook just before the request operation which then allows us to install a custom hostname verifier that performs the certificate check and, if there is a pinning related problem (such as an MITM attack), prevents the request from going out and to clear the Approov SDK's certificate cache.

Cordova Advanced HTTP uses kevinsawicki@gmail.com's HttpRequest

In file src/android/com/synconset/cordovahttp/CordovaHttp.java:

  1. Define an interface to be implemented by an interceptor. The interface's accept() method takes an HTTP request (that is ready to be sent) as its argument and can perform an action based on the contents of the request or modify the request.

    // Interface type for request interceptors
    public interface IHttpRequestInterceptor {
        public void accept(HttpRequest request);
    }
    
  2. Add the shared (between all instances of CordovaHttp) list of request interceptors

    // List of request interceptors
    private static Deque<IHttpRequestInterceptor> requestInterceptors = new LinkedList<IHttpRequestInterceptor>();
    
  3. Provide functions to add interceptors to the list and to apply all interceptors to an HTTP request. Interceptors can only be added to the front of the list to ensure that interceptors added later cannot prevent earlier added interceptors from running or cannot modify earlier changes made to the request.

    // Add a request interceptor to the list of request interceptors
    public static synchronized void addRequestInterceptor(IHttpRequestInterceptor requestInterceptor) {
        if (requestInterceptor == null) {
            throw new NullPointerException("Request interceptor must not be null");
        }
        CordovaHttp.requestInterceptors.addFirst(requestInterceptor);
    }
    

    Interceptors are applied in reverse insertion order, i.e. most recently added interceptor first.

    // Apply all request interceptors
    public static synchronized void applyRequestInterceptors(HttpRequest request) {
        for (IHttpRequestInterceptor requestInterceptor : requestInterceptors) {
            requestInterceptor.accept(request);
        }
    }
    
  4. Call the interceptors just before sending the request. In function prepareRequest() add a call to applyRequestInterceptors() as the last action of the function.

     

    protected void prepareRequest(HttpRequest request) throws HttpRequestException, JSONException {
        this.setupRedirect(request);
        this.setupSecurity(request);
        request.readTimeout(this.getRequestTimeout());
        request.acceptCharset(ACCEPTED_CHARSETS);
        request.headers(this.getHeadersMap());
        request.uncompress(true);
    

     

        // Call interceptors to allow "last-minute" changes before performing the request
        this.applyRequestInterceptors(request);
    

     

    }
    

     

IOS

For iOS things are a little less straightforward, because there one does not create a request directly, but creates a task (NSURLSessionDataTask) using a manager object that knows how to generate the request and callback functions for handling success or failure of the request operation. This means that we need to put our "request" hook just before the invocation of the manager's request generating function (GET, POST, etc), while we need a further hook in the request failure handling function, in case something went wrong with respect to pinning, e.g. if there is a MITM attack or certificate change.

Cordova Advanced HTTP uses the Alamo Fire Objective-C implementation.

In file src/ios/CordovaHttpPlugin.m:

  1. Define types for the request interceptor and the request failure interceptor. The request interceptor takes an AFHTTPSessionManager and an URL (NSString) as its arguments and can perform actions using the manager (such as adding a security policy) or the manager's response serializer (e.g. adding a request header). The request failure interceptor's arguments, an NSURLSessionTask and an NSError, can be used to determine the cause of the failure.

    // Types for request and failure interceptors
    typedef void (^RequestInterceptor)(AFHTTPSessionManager *manager, NSString *urlString);
    typedef void (^RequestFailureInterceptor)(NSURLSessionTask *task, NSError *error);
    
  2. Add the shared (between all instances of CordovaHttpPlugin) lists of request interceptors and request failure interceptors.

    // Lists of request and failure interceptors
    static NSMutableArray<RequestInterceptor>* requestInterceptors = nil;
    static NSMutableArray<RequestFailureInterceptor>* failureInterceptors = nil;
    
  3. Provide functions to add interceptors to the lists and to apply all interceptors to an AFHTTPSessionManager or NSError, respectively. Interceptors can only be added to the front of a list to ensure that interceptors added later cannot prevent earlier added interceptors from running or cannot interfere with changes made to the request by earlier added interceptors.

    // Add a request interceptor to the list of request interceptors
    + (void)addRequestInterceptor:(RequestInterceptor)requestInterceptor {
        if (requestInterceptor != nil) {
            @synchronized(requestInterceptors) {
                [requestInterceptors insertObject:requestInterceptor atIndex:0];
            }
        }
    }
    
    // Apply all request interceptors
    - (void)applyRequestInterceptorsToManager:(AFHTTPSessionManager*)manager URL:(NSString*)urlString {
        @synchronized(requestInterceptors) {
            for (RequestInterceptor requestInterceptor in requestInterceptors) {
                requestInterceptor(manager, urlString);
            }
        }
    }
    
    // Add a request failure interceptor to the list of request failure interceptors
    + (void)addRequestFailureInterceptor:(RequestFailureInterceptor)requestFailureInterceptor {
        if (requestFailureInterceptor != nil) {
            @synchronized(failureInterceptors) {
                [failureInterceptors insertObject:requestFailureInterceptor atIndex:0];
            }
        }
    }
    
    // Apply all request failure interceptors
    - (void)applyRequestFailureInterceptorsToTask:(NSURLSessionTask*)task error:(NSError*)error {
        @synchronized(failureInterceptors) {
            for (RequestFailureInterceptor requestFailureInterceptor in failureInterceptors) {
                requestFailureInterceptor(task, error);
            }
        }
    }
    
  4. Ensure the interceptors are called just before sending the request and just after a request failure. In function post: and all other functions that initiate requests, add a call to applyRequestInterceptorsToManager:URL:() as the first action of the function.

    Wrap the call to manager POST and call the interceptors before the request and in case of request failure:

     

    - (void)post:(CDVInvokedUrlCommand)command {
        ...
        manager.responseSerializer = [TextResponseSerializer serializer];
    
        // Run in background as the request interceptors that are called below may be blocking
        [self.commandDelegate runInBackground:^{
            // Call interceptors to allow "last-minute" changes before performing the request
            [self applyRequestInterceptorsToManager:manager URL:url];
    
            @try {
                [manager POST:url parameters:parameters progress:nil success:^(NSURLSessionTask task, id responseObject) {
                    NSMutableDictionary dictionary = [NSMutableDictionary dictionary];
                    [self handleSuccess:dictionary withResponse:(NSHTTPURLResponse)task.response andData:responseObject];
                    CDVPluginResult pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:dictionary];
                    [weakSelf.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId];
                } failure:^(NSURLSessionTask task, NSError error) {
    
                    // Call interceptors to allow custom error handling
                    [self applyRequestFailureInterceptorsToTask:task error:error];
    
                    NSMutableDictionary dictionary = [NSMutableDictionary dictionary];
                    [self handleError:dictionary withResponse:(NSHTTPURLResponse)task.response error:error];
                    CDVPluginResult pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsDictionary:dictionary];
                    [weakSelf.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId];
                }];
            }
            @catch (NSException *exception) {
                [self handleException:exception withCommand:command];
            }
        }];
    }
    

     

CORDOVA APPROOV HTTP - USING INTERCEPTORS TO TIE APPROOV INTO CORDOVA ADVANCED HTTP

The complete source code of Cordova Approov HTTP is available on GitHub.

CORDOVA APPROOV HTTP INTERFACE

The interface of Cordova Approov HTTP consists of just a single configuration function. Typically this function will be called once at the start of the app, but can optionally be called repeatedly, for example to add further hosts/domains to the protected set or to set the token payload to a different value.

In file www/approov-http.js:

approovConfigure(config, successCallback, errorCallback) configures the Cordova Approov HTTP plugin and calls the appropriate response function:

  • config: Map that can have the entries (among others, for full documentation see the Cordova Approov HTTP readme):

    • "customerName": String defining the customer name.

    • "tokenPayloadValue": String specifying the user-defined token payload value as an ASCII encoded string.

    • "protectedDomains": Array of maps specifying the domains to be protected by Approov. Each array-item has the entries:

      • "protectedDomainURL": String specifying the URL for the domain to protect.

      • "isMITMProtectedDomain": Boolean ("true" or "false") specifying whether the Approov token should be protected from theft through MITM attack on the connection to the mobile API server.

  • successCallback: success function callback. This function will be invoked if the call to approovConfigure() completes successfully.

  • failureCallback: Error function callback. Called with an error parameter if the configuration does not complete successfully.

CORDOVA APPROOV HTTP IMPLEMENTATION

As we have the necessary hooks for Approov to be called from Cordova Advanced HTTP, we can have a look at what code we need to execute for each Approov protected HTTPS request.

Recap: In order to use Approov with Approov token theft protection, we need to implement several bits:

  1. Retrieve an Approov token from the Approov Cloud Service
  2. Add the token to the request - as a request header
  3. Set up dynamic certificate pinning, if configured
  4. If a pinning failure is detected, clear the Approov SDK's certificate cache and re-start the token fetch, certificate retrieval and verification procedure.

ANDROID

On Android we are using a custom hostname verifier for the pinning check. This hostname verifier is called during the process of establishing a secure (TLS) connection to check that the certificate of the host to which the connection is attempted, matches the desired certificate and, if not (failure: SSL connection failed), causes the TLS handshake to be aborted and any certificates to be cleared from the Approov SDK's certificate cache.

CordovaApproovHttpUtil

CordovaApproovHttpUtil is a utility class that performs token fetching, adding the token to a request header, setting up certificate pinning, and certificate checking. We are only showing the parts of the code that are directly relevant to understanding this functionality.

In file src/android/com/criticalblue/cordova/approov/http/CordovaApproovHttpUtil.java:

Wrap the Approov SDK's fetchApproovToken call to take a URL as argument and to return an empty String if the token fetch failed.

    // If no URL is specified (url == null), fetch a generic Approov token, otherwise fetch a domain specific token for
    // the domain given in the URL.
    public static String fetchApproovToken(URL url) {
        // Set the token string to a value that signifies that no token could be obtained
        String approovToken = NO_TOKEN;

        // Fetch the token, (urlString == null) signifies generic token fetch
        String urlString = (url == null) ? null : url.toString();
        TokenInterface.ApproovResults approovAttestation = ApproovAttestation.shared().fetchApproovTokenAndWait(urlString);
        if (approovAttestation.getResult() == ApproovAttestation.AttestationResult.SUCCESS) {
            // If the fetch succeeded then we set the token string to the obtained token value
            approovToken = approovAttestation.getToken();
        }
        return approovToken;
    }

Create a custom hostname verifier that performs certificate checking, thus pinning the certificate cached by the Approov SDK. Set this certificate pinner on the connection to ensure that it is called when creating the connection.

    // Set up Approov certificate pinning
    public static void setupApproovCertPinning(HttpRequest request) throws HttpRequestException {
        // Set the hostname verifier on the connection (must be HTTPS)
        final HttpURLConnection connection = request.getConnection();
        if (!(connection instanceof HttpsURLConnection))
        {
            IOException e = new IOException("Approov protected connection must be HTTPS");
            throw new HttpRequestException(e);
        }
        final HttpsURLConnection httpsConnection = ((HttpsURLConnection) connection);

        HostnameVerifier currentVerifier = httpsConnection.getHostnameVerifier();
        if (currentVerifier instanceof CordovaApproovHttpPinningVerifier)
        {
            IOException e = new IOException("There can only be one Approov certificate pinner for a connection");
            throw new HttpRequestException(e);
        }
        // Create a hostname verifier that uses Approov's dynamic pinning approach and set it on the connection
        CordovaApproovHttpPinningVerifier verifier = new CordovaApproovHttpPinningVerifier(currentVerifier);
        httpsConnection.setHostnameVerifier(verifier);
    }

The interceptor that is using the hook in Cordova Advanced HTTP. The hook is called immediately before the HTTPS request is sent. The interceptor first checks whether the request is supposed to be Approov protected by checking the host/domain and the MITM token theft protection setting. If Approov protection is enabled, it proceeds to fetch a token and, if successful, sets up MITM protection if so configured. It finally adds the received token (which may be empty) to the header of the HTTPS request.

    // Consumer (operates via side-effects) that sets up Approov protection for a request
    public static CordovaHttpPlugin.IHttpRequestInterceptor approovProtect =
        new CordovaHttpPlugin.IHttpRequestInterceptor() {
            @Override
            public void accept(HttpRequest request) {
                URL url = request.url();
                if (isApproovProtected(url)) {
                    final boolean isMITMProtected = isApproovMITMProtected(url);
                    if (!isMITMProtected) {
                        // Indicate that a non-URL-specific token should be requested and no MITM protection should be set up
                        url = null;
                    }
                    // Fetch the Approov token
                    String approovToken = fetchApproovToken(url);
                    if (isMITMProtected && approovToken != NO_TOKEN) {
                        // Only set up dynamic cert pinning if the request is MITM protected and we could obtain a token
                        setupApproovCertPinning(request);
                    }
                    // Add Approov header containing the token to the request
                    request.header("Approov-Token", approovToken);
                }
            }
        };

CordovaApproovPinningVerifier

CordovaApproovPinningVerifier is the custom hostname verifier that apart from performing the hostname check, also checks the certificate pinning.

In file src/android/com/criticalblue/cordova/approov/http/CordovaApproovHttpPinningVerifier.java:

/**
 * Inspired by “Android Security: SSL Pinning” by Matthew Dolan
 * https://medium.com/@appmattus/android-security-ssl-pinning-1db8acb6621e
 *
 * This is an example of how to implement Approov based Dynamic Pinning
 * on Android.
 *
 * This implementation of HostnameVerifier is intended to enhance the
 * HostnameVerifier your SSL implementation normally uses. The
 * HostnameVerifier passed into the constructor continues to be executed
 * when verify is called.
 */
public final class CordovaApproovHttpPinningVerifier implements HostnameVerifier {

    /** The HostnameVerifier you would normally be using. */
    private final HostnameVerifier delegate;

    /** Tag for log messages */
    private static final String TAG = "DYNAMIC_PINNING";

    /**
     * Construct a CordovaApproovHttpPinningVerifier which delegates
     * the initial verify to a user defined HostnameVerifier before
     * applying dynamic pinning on top.
     *
     * @param delegate The HostnameVerifier to apply before the Dynamic
     *                  pinning check. Typically this would be the class
     *                  used by your usual http library (i.e OkHttp) or
     *                  simply  javax.net.ssl.DefaultHostnameVerifier
     */
    public CordovaApproovHttpPinningVerifier(HostnameVerifier delegate) {
        this.delegate = delegate;
    }

The checkDynamicPinning() function first ensures that there is a certificate to use in the Approov SDK's certificate cache by calling fetchApproovToken() if required. It then compares the cached certificate with the leaf certificate obtained from the session. If the certificates match, the function returns true. Otherwise the Approov SDK's certificate cache is cleared to allow recovery from certificate change and the function returns false to indicate pinning failure.

    /**
     * Check the Approov SDK cached cert for this hostname
     * against the provided Leaf Cert.
     *
     * @param hostname Name of the host we are checking the cert for.
     * @param leafCert The leaf certificate of the chain provided by the
     *                  host we are connecting to. Typically this is the 0th
     *                  element it the certificate array.
     * @return true if the the certificates match, false otherwise.
     */
    private boolean checkDynamicPinning(String hostname, Certificate leafCert) {

        // Check if we have the cert for the hostname in the sdk cache
        if (ApproovAttestation.shared().getCert(hostname) == null) {
            // Do the token fetch that we must have missed previously.
            ApproovAttestation.AttestationResult result = ApproovAttestation.shared()
                    .fetchApproovTokenAndWait(hostname).getResult();
            // If the fetch failed then we give up
            if (result == ApproovAttestation.AttestationResult.FAILURE) {
                return false;
            }
        }

        // This should always work now.
        byte[] certBytes = ApproovAttestation.shared().getCert(hostname);
        if (certBytes == null) {
            return false;
        }

        // Convert bytes into cert for comparison
        try {
            CertificateFactory cf = CertificateFactory.getInstance("X.509");
            Certificate cert = cf.generateCertificate(new ByteArrayInputStream(certBytes));

            if (cert.equals(leafCert)) {
                return true;
            } else {
                // We need to flush the cert cache so that we can recover from failure due to certificate change
                ApproovAttestation.shared().clearCerts();
                return false;
            }
        } catch (CertificateException e) {
            // We need to flush the cert cache so that we can recover from failure due to certificate change
            ApproovAttestation.shared().clearCerts();
            return false;
        }
    }

The verify() method is called during establishing an SSL connection. Our custom hostname verifier overrides this method, but before performing its pinning check, calls the original verify() method of the superclass. It then calls our certificate pinner with the leaf certificate obtained from the SSL session.

    @Override
    public boolean verify(String hostname, SSLSession session) {
        if (delegate.verify(hostname, session)) try {
            // Assume the leaf cert is at element 0 in the getPeerCertificates() array.
            return checkDynamicPinning(hostname, session.getPeerCertificates()[0]);
        } catch (SSLException e) {
            throw new RuntimeException(e);
        }

        return false;
    }
}

IOS

The full iOS source code of Cordova Approov HTTP is available on GitHub.

As for Android, we are only showing the bits that deal with token fetching, adding token to request, setting up certificate pinning and checking certificates.

Remember that for iOS things are slightly different in that we have to implement two interceptors. Because we cannot add an Approov token to a request header directly, we need to create a security policy which can be set in the manager that generates the request. We cache these security policies (using an instance called serverSecurityPolicyManager of class CriticalBlueServerSecurityPolicyManager, not shown) so they don't need to be recreated for every request sent.

In file src/ios/CordovaApproovHttpPlugin.m:

Fetch an Approov token, retrieve the certificate on the connection to the mobile API server from the Approov SDK and update the cached security policy for the connected server.

    // If no URL is specified (url == nil), fetch a generic Approov token.
    // Otherwise fetch a domain specific token for the domain given in the URL and, if successful, update the security
    // policy cache using the domain's certificate. If no certificate can be obtained (leafCertData == nil), the security
    // policy for the domain is removed from the cache.
    - (NSString*)fetchApproovTokenAndUpdateSecurityPolicyForURL: (NSURL*)url {
        // Set the token string to a value that signifies that no token could be obtained
        NSString *approovToken = NO_TOKEN;
        // Fetch the token for the domain we are about to access, (url == null) signifies generic token fetch
        ApproovTokenFetchData *approovData = [[ApproovAttestee sharedAttestee] fetchApproovTokenAndWait: [url absoluteString]];
        switch (approovData.result) {
            case ApproovTokenFetchResultSuccessful:
            {
                approovToken = approovData.approovToken;
                if (url != nil) {
                    NSData* leafCertData = [[ApproovAttestee sharedAttestee] getCert: [url absoluteString]];
                    // Update security policy cache. If (leafCertData == nil), remove any security policy for the domain.
                    [serverSecurityPolicyManager updateServerSecurityPolicyCacheForHost: [url host] withCert: leafCertData];
                }
                break;
            }
            default:
              break;
        }
        return approovToken;
    }

Add the token to the request header and set up certificate pinning if configured.

In method pluginInitialize:

The interceptor uses the request hook in Cordova Advanced HTTP which is called immediately before the manager's HTTPS request method is invoked. The interceptor first checks whether the request is supposed to be Approov protected by checking the host/domain and the MITM token theft protection setting. If Approov protection is enabled, it proceeds to fetch a token and update the cached security policy for pinning, then, if successful, it sets up the pin for the MITM protection if configured. It finally adds the received token to the manager's request-serializer's list of header fields, so that when the request is created, the token is included.

    approovProtect = ^(AFHTTPSessionManager *manager, NSString *urlString) {
        NSURL *url = [NSURL URLWithString: urlString];
        if ([weakSelf isApproovProtectedURL: url]) {
            BOOL isMITMProtected = [weakSelf isApproovMITMProtectedURL: url];
            if (!isMITMProtected) {
                // Indicate that a non-URL-specific token should be requested and no MITM protection should be set up
                url = nil;
            }
            // Fetch the Approov token, check for certificate change and update the server security policy cache.
            NSString *approovToken = [weakSelf fetchApproovTokenAndUpdateSecurityPolicyForURL: url];
            if (isMITMProtected && approovToken != NO_TOKEN) {
                // Only set up dynamic cert pinning if the URL is MITM protected and we could obtain a token
                [manager setSecurityPolicy: [weakServerSecurityPolicyManager serverSecurityPolicyForHost: [url host]]];
            }
            // Add Approov header containing the token
            [manager.requestSerializer setValue: approovToken forHTTPHeaderField: @"Approov-Token"];
        }
    };

Check the mobile API server's certificate and, if the pinning check fails (failure: cancelled), prevent the request from going out and clear the Approov SDK's certificate cache. The certificate pinning is checked inside iOS using the security policy set on the manager. If the request fails because of certificate pinning, any Approov token is safe , because the request isn't sent. Also the following request failure interceptor (which is installed in Cordova Advanced HTTP's interceptor hook for the failure handler) is called.

In method pluginInitialize:

The failure handler first checks whether the failure may have been caused by pinning and if so, clears the Approov SDK's certificate cache (and also the server security policy cache whose contents are now out of date) to allow recovery from certificate change on the next attempted request.

    approovFailureHandler = ^(NSURLSessionTask *task, NSError *error) {
        /* If the Alamofire response was a failure with a 'cancelled' error type and we didn't get an iOS HTTPURLResponse,
           then assume this could be because of an issue with certificate pinning, so clear the Approov-verified
           certificates and the server security policy cache */
        if (error == nil || ([error code] == NSURLErrorCancelled && task.response == nil)) {
            [[ApproovAttestee sharedAttestee] clearCerts];
            [weakServerSecurityPolicyManager clearServerSecurityPolicyCache];
        }
    };

TYING IT ALL TOGETHER - USING APPROOV IN A CORDOVA APP

Phew, that is a lot of stuff. But it's all for the purpose of making the integration into an actual app simple. It will also make the integration of Approov or some other service with an app development platform easier next time, as we can re-use the ideas and the code that we have written.

Now that the integration of Approov mobile API protection with the Cordova platform is complete and available for download, what is left is to do the easy bit. We can now integrate Approov into a mobile app like so:

In the code of the mobile app that handles the device ready event, add a call to configure Approov:

if ("deviceready" == id) {
    // Configure Approov. For details, please see the plugin documentation.
    var config = {
        "customerName": "me",
        "protectedDomains": [
            {
                "protectedDomainURL": "https://my.domain.com/endpoint",
                "isMITMProtectedDomain": "true"
            },
            // You can add more domains to protect here
        ]
    };
    cordova.plugin.approov.http.approovConfigure(
        config,
        function() {
            // Success
            console.log("Successfully configured Approov HTTP Plugin");
        },
        function(error) {
            // Failure
            console.log("Error configuring Approov HTTP Plugin: " + error);
        });

This configures Cordova Approov HTTP to Approov-protect any request to "my.domain.com" and to prevent theft of valid Approov tokens through a MITM attack. All requests to other domains will not be affected.

Cordova Advanced HTTP can be used in the same way as before, but with Approov protection in place - enabling the API server to reject requests that do not originate from a bona-fide app.
Example call to Cordova Advanced HTTP's get request function (note: no changes from what we would normally do):

cordova.plugin.http.get("https://my.domain.com/endpoint", {}, {}, 
    function(response) {
        // Success
        if (response.status == 200) {
            console.log("Successfully performed GET request");
        }
    },
    function(response) {
        if (response.status != 200) {
            // Failure
            console.log("Error on GET request: " + response.status);
        }
    });

BUILDING AND RUNNING AN APP

When building the mobile app it is important to ensure that the modified Cordova Advanced HTTP with the interceptor hooks is picked up by Cordova, not the original Cordova Advanced HTTP plugin. You also need to include the Cordova Approov HTTP plugin from GitHub or from NPM in your Cordova project. Then build the mobile app as normal.

You can now run the mobile app, but it will not authenticate until you have registered it with the Approov Cloud Service.

  • An Approov subscription is required for this - it's free for a month, you can sign up here
  • You also need the Approov registration tools which you can download from the Approov Admin Service once you have your trial subscription.

Once the mobile app is registered and after a short propagation delay of no more than 30s, the mobile app will be recognized as valid by our service and will be issued tokens that your mobile API server can check for validity in order to reject bogus traffic.

Note: Attaching a debugger or using a rooted device will be detected by the Approov SDK and you will not get a valid token.

You can find detailed instructions about building and registering an app in the readmes for Cordova Approov HTTP and the Cordova Approov HTTP Demo.

WHERE TO GO FROM HERE

  • The Cordova Approov HTTP Demo shows how to use the Cordova Approov HTTP plugin to add Approov Mobile API Protection to requests made through Cordova Advanced HTTP. If you want to try this, please refer to the instructions in the demo's README.md. It gives you the chance to see a working system consisting of an app that uses Cordova Advanced HTTP and Cordova Approov HTTP, the Approov Cloud Service and an example mobile API server without having to sign up for a trial subscription.

  • The Approov server-side integration documentation guides you through the server-side code required to receive and validate tokens.

  • Do an integration of your own app using Approov's free trial.

  • Create another platform integration of Approov

That's it, thanks for listening.

FURTHER INFORMATION

Please also see the full Approov documentation.

If you have any questions or problems, please just get in touch via Zendesk.

 

Test Drive Approov!