Approov MITM Detection

The Approov SDK provides the capability to detect if the HTTPS connection between your app and your API has been intercepted by a Man in the Middle (MITM) attack. This mechanism allows you to detect on your server side that the connection is not secure and prevents the theft and reuse of valid Approov tokens. Privacy of data transmitted from your app to your API is not guaranteed by this capability.

When YourApp wants to talk to your server's API endpoint at yourapi.com/endpoint, it asks the Approov SDK contained within it for a token for yourapi.com. The SDK asks the Approov Cloud Service for a token that allows YourApp to talk to yourapi.com. The cloud service sends the SDK a token, the SDK passes it on to YourApp, and YourApp sends it together with some data to yourapi.com. Your server checks if the token is valid and if it is, it sends some data back to YourApp.

There are 2 components to the system.

  • The fetchApproovToken method calls take a hostname parameter. Both the SDK in the app and the Approov server will fetch the https certificate from the hostname. If the certificates do not match, the Approov token will not validate.
  • The SDK provides the getCert(hostname) method which returns the certificate for the supplied hostname as fetched by the SDK. This can then be used in your app to implement certificate pinning.

We strongly recommend Certificate Pinning (or Dynamic Pinning with MITM Detection described below) for any HTTPS connection over which you are intending to send Approov tokens. Failure to do so may result in token theft and reuse on your API.

Approov MITM Detection

The Man in the Middle (MITM) detection feature of Approov takes advantage of the secure connection between the Approov SDK and cloud service. Since the SDK is on the device, it can always be proxied one way or another. The cloud service can detect this and will not issue a valid token over a connection which is being intercepted.

This trusted connection allows the Cloud service and the SDK to compare their respective views of any API endpoint by independently fetching the TLS certificate for the host, as used in establishing a HTTPS connection. If the two certificates match then the connection between the API and the SDK, and therefore the app, is secure. If they do not then the connection between the app and the API is being intercepted by a proxy. In this case only invalid tokens will be generated by the cloud service and sent to the SDK. The diagrams below illustrate the approach.

A secure connection between your app and API has matching certificates on both fetches:

YourApp calls the Approov SDK's fetchApproovToken(yourapi.com). The SDK fetches the TLS certificate from yourapi.com, and sends it to the Approov Cloud Service as part of the Approov Attestation over a trusted connection. The cloud service also fetches your server's TLS certificate. It compares it to the one it got from the SDK. Both need to match for the cloud service to respond with a valid token, which the SDK passes on to your app.

A connection that has been intercepted by a proxy (man-in-the-middled) does not, so it results in invalid Approov tokens:

If there is a man-in-the-middle between YourApp and yourapi.com, the SDK's certificate fetch receives the attacker's TLS certificate, and sends that to the Approov Cloud Service. The cloud service gets the real certificate from your server, sees that they don't match, and responds to the SDK with an invalid Approov token, which the SDK passes on to your app.

Dynamic Pinning with MITM Detection

Traditional static pinning techniques specify the certificate you are expecting from your API server and ensure that secure connections are only made to that server, thus blocking MITM attempts. If you wish to ensure the privacy of data other than the Approov token then this approach is still necessary. Without statically defining the certificate in the app, Approov detects if MITM is occurring - and if so, it ensures that any transmitted Approov token will be an invalid one.

On your server side, you do the usual check. If the connection from your app to API is being man-in-the-middled, the token provided by Approov will not validate when you test it, so you can refuse to respond with data you wish to keep secure. Since the token is invalid, it does not matter if it is stolen and reused. These scenarios are shown below.

When the connection is secure, your API server does the standard JWT check on the Approov token it has received, and establishes that it is valid. It can respond with sensitive data:

When the connection between YourApp and yourapi.com is secure, the Approov SDK will have responded with a valid token. YourApp can send that in an API request to your server together with some data. Your server performs a JWT token check and ensures the token is valid, then responds to YourApp.

When the connection has been intercepted, your server has received an invalid token from your app. It knows the connection has been compromised, so it takes its chosen course of action in response. The data sent by your app is still visible to the MITM proxy:

When the connection between YourApp and yourapi.com has been man-in-the-middled, the Approov SDK will have responded with an invalid token. YourApp sends that in an API request to your server together with some data. The attacker can read the data as it comes in, then redirect it to yourapi.com as it is, or modify the data. Your server knows the connection is man-in-the-middle because it checks the token and finds that it is invalid, so it can treat this request differently.

Further, if the SDK cannot retrieve a valid TLS certificate for a host at all, then a failure status will be returned and no token will be available.

MITM detection is enabled by simply specifying the hostname of the API endpoint you are about to access as a parameter to the corresponding call to fetchApproovToken.

SDK Cache

As each call is made, the SDK will fetch and cache the leaf certificate from the specified host. Over time this cache will become populated with a certificate for each host your app uses Approov to authorize with. You then proceed with the token in the usual way by adding it to the request header.

This certificate cache avoids having to generate excessive traffic to your API, but it also means that:

  • it is necessary to always check for any change to a protected host’s certificate each time you connect to it.
  • you have to clear the cache with a call to clearCerts() whenever there is a certificate change for any of the protected hosts.

These goals can be accomplished via a variation of the usual Certificate Pinning approach, see Implementation and Examples.

The first call to fetchApproovToken(api1.com) will fetch the TLS certificate for api1. api1 will then receive valid Approov tokens from the app if the connection between them is secure:

The first time your app calls the SDK method fetchApproovToken for api1.com, the Approov SDK does a TLS certificate fetch from api1.com. The certificate is cached in the SDK. Your app makes a request to api1.com with a valid Approov token and receives a normal response.

With requests to different APIs, the cache gets populated with their certificates to prevent unnecessary requests. Your app will be able to access these certificates via the getCert method (see Implementation):

While the communication between your app and api1.com continues, the app calls the SDK method fetchApproovToken for api2.com. The SDK fetches the TLS certificate from api2.com, and caches it. The cache now contains the certificates for api1.com and api2.com. Requests from your app to api1.com and api2.com carry a valid Approov token, so both servers respond normally.

api1 gets man-in-the-middled, so the SDK starts issuing invalid Approv tokens. Until the cache is cleared, all Approov-protected end-points, api1 and api2 in this example, will receive an invalid token. They will therefore be aware that the app is not to be trusted:

The connection between the app and api1.com is man-in-the-middled. The TLS certificate fetch returns the attacker's certificate, which goes into the SDK cache. The connection between the app and api2.com is not man-in-the-middled, so the TLS certificate fetch returns the actual server's certificate. The SDK cache now contains one valid and one invalid certificate, so Approov tokens are invalid. Your app makes requests to both api1.com and api2.com with the same invalid token. The man-in-the-middle redirects the token to api1.com, which treats the request as compromised. Until the cache is cleared, api2.com will also distrust requests.

Implementation

Static pinning can be difficult to manage operationally due to dependencies between app and server so instead you can make use of the fact the SDK has cached a copy of the certificate for your API, when you first make a call to fetchApproovToken(hostname), to implement pinning dynamically at runtime.

This capability is enabled by the following methods on the ApproovAttestation.shared() object.

  • getCert(String hostname) returns the certificate for the specified hostname from those cached by the SDK. If the SDK could not access the hostname then this method returns null.

  • clearCerts() clears the SDK cache. Subsequent calls to fetchApproovToken(hostname) will result in a new certificate for the specified host being fetched. Calls to getCert(hostname) will return null until a corresponding fetchApproovToken(hostname) call as been made.

    This functionality allows you to then create a pinning scheme on the fly for your endpoints which will both adapt to legitimate certificate changes on your endpoint and also block attempts to MITM the connection dynamically. This can be implemented in a straightforward manner on most platforms by customizing elements of trust management for the HTTPS stack, see Examples.

Note

Certificate Check: The server side of the Approov Service currently checks the leaf certificate for each hostname. You must ensure that all endpoints worldwide which resolve the hostname have the same certificate.

Development APIs: It is common to have a development version of an API which is not publicly visible on the web. When developing against such an API, the Approov servers will not be able to obtain a certificate for comparisons and associated attestations will fail. To resolve this issue, we suggest that while developing against the internal API you pass a null parameter value into this method (or equivalent for your language). Once testing moves to a publicly visible service you can switch bak to using the real hostname value.

Examples

Android HostnameVerifier
package com.criticalblue.demo;

import android.util.Log;

import com.criticalblue.attestationlibrary.ApproovAttestation;
import com.criticalblue.attestationlibrary.TokenInterface;

import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLSession;
import javax.net.ssl.SSLException;

import java.io.ByteArrayInputStream;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;

/**
 * Created by barryo on 07/08/17.
 *
 * 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.
 *
 *
 * Use Cases:
 *
 * -- HttpsURLConnection --
 *
 *  // Override the default Hostname verifier for new HTTPS connections
 *  URL aUrl = new URL("https://" + SERVER_HOSTNAME);
 *  HttpsURLConnection connection = (HttpsURLConnection) aUrl.openConnection();
 *  DynamicPinningHostnameVerifier verifier = new DynamicPinningHostnameVerifier(connection.getHostnameVerifier());
 *  connection.setHostnameVerifier(verifier);
 *
 *  // Create new connections as usual.
 *
 * -- OkHttp --
 *
 *  // Use the default HostnameVerifier from OkHttpClient
 *  OkHttpClient baseClient = new OkHttpClient();
 *
 *  // Build a new instance of the okHttpClient for this requester to use.
 *  DynamicPinningHostnameVerifier verifier
 *      = new DynamicPinningHostnameVerifier(baseClient.hostnameVerifier());
 *  pinnedClient = baseClient.newBuilder()
 *                  .hostnameVerifier(verifier)
 *                  .build();
 *
 *  When Executing a request, always catch SSLPeerUnverifiedException and perform
 *  a retry including a call to ApproovAttestation.shared().fetchApproovToken..(String hostname)
 */

public final class DynamicPinningHostnameVerifier 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 DynamicPinningHostnameVerifier 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 DynamicPinningHostnameVerifier(HostnameVerifier delegate) {
        this.delegate = delegate;
    }

    /**
     * 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) {
            Log.w(TAG, "Approov SDK does not have a cached cert for: " + hostname);
            Log.i(TAG, "Running Token Fetch to get cert for: " + hostname);
            // 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) {
                Log.e(TAG, "Cannot fetch a cert for: " + hostname);
                return false;
            }
        }

        // This should always work now.
        byte[] certBytes = ApproovAttestation.shared().getCert(hostname);
        if (certBytes == null) {
            Log.e(TAG, "Cannot fetch a cert for: " + hostname);
            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)) {
                Log.i(TAG, "Pinning check passed for " + hostname);
                return true;
            } else {
                // We need to flush the cert cache so that connections to other hosts don't fail just because this one failed the cert check
                Log.w(TAG, "Certs do not match for: " + hostname + " - flushing SDK cert cache.");
                ApproovAttestation.shared().clearCerts();
                return false;
            }

        } catch (CertificateException e) {
            // We need to flush the cert cache so that connections to other hosts don't fail just because this one failed to get a cert
            Log.w(TAG, "Failed to construct Certificate object from bytes for: " + hostname + " - flushing SDK cert cache.");
            ApproovAttestation.shared().clearCerts();
            return false;
        }


    }

    @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 AlamoFire
/*****************************************************************************
 * Project:     AFCertPinning
 * File:        AppDelegate.swift
 * Original:    Created on 24 July 2017 by Simon Rigg
 * Copyright(c) 2002 - 2017 by CriticalBlue Ltd.
 *****************************************************************************/

import UIKit
import Alamofire
import Toast
import Approov

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

    /* The shared AppDelegate instance */
    static var shared: AppDelegate {
        return UIApplication.shared.delegate as! AppDelegate
    }
    
    /* The application window */
    var window: UIWindow?
    
    /* The customised Alamofire SessionManager with Approov-verified certificate pinning support */
    var alamoSessionManager: SessionManager!
    
    /* The customised Alamofire ServerTrustPolicyManager */
    fileprivate var alamoTrustPolicyManager: CriticalBlueServerTrustPolicyManager!

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
        
        /* configure AlamoFire for certificate pinning */
        self.alamoTrustPolicyManager = CriticalBlueServerTrustPolicyManager(policies: [:])
        self.alamoSessionManager = SessionManager(
            configuration: URLSessionConfiguration.ephemeral,
            delegate: SessionDelegate(),
            serverTrustPolicyManager: self.alamoTrustPolicyManager
        )
        
        return true
    }
}

/**
 * A custom Alamofire ServerTrustPolicyManager class which builds ServerTrustPolicy objects on the fly
 * using the X.509 DER certificate data verified by Approov for a particular API request.
 */
fileprivate class CriticalBlueServerTrustPolicyManager: ServerTrustPolicyManager {
    
    /* create a ServerTrustPolicy which always fails */
    let failureServerTrustPolicy = ServerTrustPolicy.customEvaluation {(trust, host) -> Bool in
        return false
    }
    
    /* cache of X.509 DER certificate data/ServerTrustPolicy tuples for host names */
    var serverTrustPolicyCache = [String:(leafCertData:Data, policy:ServerTrustPolicy)]()
    
    /* dispatch queue used to protect multi-threaded accesses to 'serverTrustPolicyCache' using a shared exclusion lock */
    let serverTPCUpdateQueue = DispatchQueue(label: "TPCUpdateQueue", attributes: .concurrent)
    
    override func serverTrustPolicy(forHost host: String) -> ServerTrustPolicy? {
        
        /* return the cached server trust policy, otherwise a failure policy */
        var serverTrustPolicy: ServerTrustPolicy?
        serverTPCUpdateQueue.sync() {
            serverTrustPolicy = serverTrustPolicyCache[host]?.policy ?? failureServerTrustPolicy
        }
        return serverTrustPolicy
    }
    
    func updateServerTrustPolicyCache(host: String, leafCertData: Data) {
        
        /* first check to see if there is a difference in certificates */
        let oldLeafCertData = serverTrustPolicyCache[host]?.leafCertData ?? nil
        if leafCertData != oldLeafCertData {
            /* build the Alamofire security policy using the Approov-verified X.509 DER certificate data */
            if let serverCert = SecCertificateCreateWithData(nil, leafCertData as CFData) {
                let serverTrustPolicy = ServerTrustPolicy.pinCertificates(
                    certificates: [serverCert],
                    validateCertificateChain: true,
                    validateHost: true
                )
                serverTPCUpdateQueue.async(flags: .barrier) {
                    self.serverTrustPolicyCache[host] = (leafCertData, serverTrustPolicy)
                }
            }
            else {
                serverTPCUpdateQueue.async(flags: .barrier) {
                    self.serverTrustPolicyCache[host] = nil
                }
            }
        }
    }
    
    func clearServerTrustPolicyCache(host: String) {
        
        /* clearing the cache entry will result in the 'failure' server trust policy being used */
        serverTPCUpdateQueue.async(flags: .barrier) {
            self.serverTrustPolicyCache[host] = nil
        }
    }
    
    func clearEntireServerTrustPolicyCache() {
        
        /* clearing the cache entry will result in the 'failure' server trust policy being used */
        serverTPCUpdateQueue.async(flags: .barrier) {
            self.serverTrustPolicyCache.removeAll()
        }
    }
}

/**
 * An Alamofire SessionManager extension to allow API requests to include an Approov token header.
 */
extension SessionManager {
    
    open func approovRequest(_ url: URLConvertible, method: Alamofire.HTTPMethod = .get, parameters: Parameters? = nil,
        encoding: ParameterEncoding = JSONEncoding.default, headers: HTTPHeaders? = nil) -> Alamofire.DataRequest {
        
        /* attempt to fetch an Approov token for the given URL; on success, add the X.509 DER certificate data to our
           pinning cache with a new trust policy then add the Approov token to an 'Approov-Token' request header; on
           failure, clear the pinning cache for the URL */
        var httpHeaders = headers ?? [String:String]()
        if let urlObject = try? url.asURL(), let host = urlObject.host {
            if let approovData = ApproovAttestee.shared()?.fetchApproovTokenAndWait(urlObject.absoluteString), approovData.result == .successful,
            let leafCertData = ApproovAttestee.shared()?.getCert(urlObject.absoluteString) {
                AppDelegate.shared.alamoTrustPolicyManager.updateServerTrustPolicyCache(host: host, leafCertData: leafCertData)
                httpHeaders["Approov-Token"] = approovData.approovToken
            }
            else {
                AppDelegate.shared.alamoTrustPolicyManager.clearServerTrustPolicyCache(host: host)
            }
        }
        
        /* build the new request object (which we will return) and add a response handler here so we can deal with
           failures and update the server trust policies accordingly */
        let request = self.request(url, method: method, parameters: parameters, encoding: encoding, headers: httpHeaders)
        request.response { response in
            /* 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 trust policy cache */
            if (response.error as NSError?)?.code == NSURLErrorCancelled && response.response == nil {
                ApproovAttestee.shared()?.clearCerts()
                AppDelegate.shared.alamoTrustPolicyManager.clearEntireServerTrustPolicyCache()
            }
        }
        return request
    }
}

The Pinning implementation can then be integrated with the fetchApproovToken and API request routine in a straightforward manner. To ensure that dynamic pinning can cope with legitimate changes to the host certificate, which will typically happen at least once per year, it is important that the code retries the request after a pinning failure. Since the implementations above call clearCerts() a second call to fetchApproovToken(hostname) is necessary to prompt the SDK to update the certificate for the host. The following examples illustrate how to implement this retry functionality in your app along with the integration of the hostname checks provided above.

Android OkHttp3
public class RequestShape extends Activity {

    // Local customized HttpClient
    OkHttpClient httpClient;

    // URLs for our API
    static final String DEMO_SERVER_HOSTNAME = "demo-server.approovr.io";
    static final String DEMO_SERVER_ENDPOINT = "https://" + DEMO_SERVER_HOSTNAME + "/resultFromResponses";

    // Log tag for searching in logcat
    static final String TAG = "RequestShape";

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // Create a custom HostnameVerifier which supports our dynamic pinning approach.
        // This uses the current HostnameVerifier from OkHttpClient
        DynamicPinningHostnameVerifier pinningHostnameVerifier
                = new DynamicPinningHostnameVerifier(new OkHttpClient().hostnameVerifier());

        // Build a new instance of the okHttpClient for this requester to use.
        httpClient = new OkHttpClient.Builder()
                .hostnameVerifier(pinningHostnameVerifier)
                .build();
    }

    /**
     * The function that calls the API we protect with Approov.
     */
    public void okHttpRequestShape() {
        // Run our HTTP request in a background thread to avoid blocking the UI thread
        AsyncTask.execute(new Runnable() {
            @Override
            public void run() {

                String resultFromResponse = "none";
                // We may need to retry if there is a pinning failure the first time -
                // to handle legitimate server-side changes to the certificate
                boolean retry = false;
                while (true) {
                    // Fetch an Approov Token using the SDK
                    ApproovResults approovResults = ApproovAttestation.shared().fetchApproovTokenAndWait(DEMO_SERVER_HOSTNAME);

                    // Check that the token was fetched successfully
                    String token;
                    if (approovResults.getResult() == ApproovAttestation.AttestationResult.SUCCESS) {
                        token = approovResults.getToken();
                    } else {
                        // A fail here means that the SDK could not reach the Approov servers
                        // before timing out. Set the token field to a known value to communicate
                        // this state (rather than leaving empty or excluding from the header)
                        Log.w(TAG, "Approov SDK token fetch failed");
                        token = "NOTOKEN";
                    }

                    // Create a request to send to our API endpoint
                    Request request;
                    try {
                        request = new Request.Builder()
                                .url(DEMO_SERVER_ENDPOINT)
                                .addHeader("Approov-Token", token)
                                .get()
                                .build();
                    } catch (IllegalArgumentException ex) {
                        Log.e(TAG, ex.getMessage());
                        return;
                    }

                    // Send the request to our API endpoint
                    try (Response response = httpClient.newCall(request).execute()) {
                        if (response.isSuccessful()) {
                            Log.i(TAG, "Successfully got response from our API");
                            // Handle success here:
                            resultFromResponse = response.body().string();
                        } else {
                            // Our API returned an error code
                            Log.e(TAG, "Error code on GET request: " + Integer.toString(response.code()));
                            return;
                        }
                        // Success - break the retry loop
                        break;

                    } catch (SSLPeerUnverifiedException ex) {
                        // Pinning test failed.
                        // This happens if the certificate from the server
                        // does not match the one cached in the SDK
                        if (!retry) {
                            // This is our first attempt so
                            // go around the retry loop, including the token
                            // and certificate refetch.
                            Log.w(TAG, "Server Cert mismatch. Retrying");
                            retry = true;
                        } else {
                            // This was our second attempt so
                            // we cannot get the pinning check to pass.
                            // Break without setting the result
                            Log.e(TAG, "Server Cert mismatch retry failed.");
                            break;
                        }
                    } catch (IOException ex) {
                        // Log an error and go around the retry loop again
                        Log.e(TAG, ex.getMessage());
                    }
                }

                if !resultFromResponse.equals("none") {
                  // Continue with the result from our API ...
                }
            }
        });

    }
}
Android HttpsUrlConnection
// URLs for our API
static final String DEMO_SERVER_HOSTNAME = "demo-server.approovr.io";
static final String DEMO_SERVER_ENDPOINT = "https://" + DEMO_SERVER_HOSTNAME + "/resultFromResponses";

// Log tag for searching in logcat
static final String TAG = "RequestShape";

/*
* The function that calls the API we protect with Approov.
*/
public void defaultRequestShape() {
    // Run our HTTP request in a background thread to avoid blocking the UI thread
    AsyncTask.execute(new Runnable() {
        @Override
        public void run() {

            String resultFromResponse = "none";
            // We may need to retry if there is a pinning failure the first time -
            // to handle legitimate server-side changes to the certificate
            boolean retry = false;
            while (true) {
                // Fetch an Approov Token using the SDK
                ApproovResults approovResults
                    = ApproovAttestation.shared().fetchApproovTokenAndWait(DEMO_SERVER_HOSTNAME);

                // Check that the token was fetched successfully
                String token;
                if (approovResults.getResult() == ApproovAttestation.AttestationResult.SUCCESS) {
                    token = approovResults.getToken();
                } else {
                    // A fail here means that the SDK could not reach the Approov servers
                    // before timing out. Set the token field to a known value to communicate
                    // this state (rather than leaving empty or excluding from the header)
                    Log.w(TAG, "Approov SDK token fetch failed");
                    token = "NOTOKEN";
                }

                HttpsURLConnection connection = null;
                try {
                    // Create a request to send to our API endpoint
                    URL aUrl = new URL(DEMO_SERVER_ENDPOINT);
                    // Get our HTTPS connection object
                    connection = (HttpsURLConnection) aUrl.openConnection();
                    // Set this request as a GET
                    connection.setRequestMethod("GET");
                    // Set a timeout for the request
                    connection.setConnectTimeout(500);

                    // Create a hostname verifier using our dynamic pinning approach
                    DynamicPinningHostnameVerifier verifier
                        = new DynamicPinningHostnameVerifier(connection.getHostnameVerifier());
                    connection.setHostnameVerifier(verifier);

                    connection.addRequestProperty("Approov-Token", token);

                    // Send the request to our API endpoint
                    int responseCode = connection.getResponseCode();
                    if(responseCode == 200) {
                        Log.i(TAG, "Successfully got response from our API");
                        // Handle success here:
                        resultFromResponse = readHttpInputStreamToString(connection);
                    } else {
                        // Our API returned an error code
                        Log.e(TAG, "Error code on GET request: " + Integer.toString(responseCode));
                        return;
                    }
                    // Success - break the retry loop
                    break;
                } catch (SSLPeerUnverifiedException ex) {
                    // Pinning test failed.
                    // This happens if the certificate from the server
                    // does not match the one cached in the SDK
                    if (!retry) {
                        // this is our first attempt so
                        // go around the retry loop, including the token
                        // and certificate refetch.
                        Log.w(TAG, "Server Cert mismatch. Retrying");
                        retry = true;
                    } else {
                        // This was our second attempt so
                        // we cannot get the pinning check to pass.
                        // Break without setting the result
                        Log.e(TAG, "Server Cert mismatch retry failed.");
                        break;
                    }
                } catch(Exception e) {
                    if(connection != null)
                        connection.disconnect();
                }
            }

            if !resultFromResponse.equals("none") {
              // Continue with the result from our API ...
            }
        }
    });
}

Once these defences are in place it will be extremely difficult for someone to intercept and reuse a valid Approov token to access your API. The combination of the token check on your server side and dynamic pinning on the connection ensures that data transmitted from your API to the app cannot be modified in flight.