I've recently been looking into ways to improve the security of mobile apps and services with my colleague Julien. One of the vendors we've evaluated is https://www.approov.io/ who provide an API Protection solution that enables a backend service to confirm it is talking to a genuine, untampered app running on a genuine, untampered (not jailbroken, no mitm proxy etc.) mobile device. On Android it could be considered as an enhancement to the protection offered by Google SafetyNet Attestation but, unlike SafetyNet, Approov also supports iOS.
Approov PoC
One of the benefits of Approov is that it allows you to remove embedded client secrets from your mobile app. Instead you attach a short-lived Approov JSON Web Token (JWT) token - issued by the Approov cloud service - to your mobile app's API requests and your API gateway validates the token and allows the request to proceed (or not). We trialled the Approov solution and implemented a PoC on Android and iOS.
It works as follows:
Figure: https://www.approov.io
The changes we had to make to support Approov weren't huge but larger than a typical SDK where you just call a configuration function at app launch.
- Replace client secret in app with dummy
- Configure Approov SDK at app launch – with embedded initial config file and dynamic config
- Fetch Approov token from SDK and attach it to outgoing requests
- Store updated Approov dynamic config
The Approov dynamic config allows us to do dynamic pinning which is what I want to focus on in this blog post.
But first, what's pinning and are there any pitfalls?
A mobile client can check that it is talking to a genuine server before allowing a request to proceed by validating the server certificate / public key. This protection mechanism is recommended by OWASP (probably the main folk to consult about mobile app security) in their Mobile Application Security Verification Standard as Level 2 'Defense-in-Depth' protection.
Figure: https://mobile-security.gitbook.io
L2 introduces advanced security controls that go beyond the standard requirements. To fulfill MASVS-L2, a threat model must exist, and security must be an integral part of the app's architecture and design. Based on the threat model, the right MASVS-L2 controls should have been selected and implemented successfully. This level is appropriate for apps that handle highly sensitive data, such as mobile banking apps.
Figure: https://mobile-security.gitbook.io
Normally the server certificate / public key is embedded in the app binary (e.g. as a pem file or hard-coded pins in source code) by the developer. Embedding a key or certificate in your app means that it could be left unable to make requests (oh, oh) if the server-side certificate changes - either through normal certificate rotation or due to a security breach. To guard against this, app developers need to ensure that the released app contains the up-to-date server certificate. They can do this by including multiple certificates and/or making sure they release an app with the 'next' server certificate before it goes live on the server. But it's difficult to manage and the potential for trouble is high...
An alternative is to do it dynamically - which, handily, Approov facilitates:
Figure: https://www.approov.io
Dynamic pinning (with Approov)
The Approov cli tool allows you to add domains e.g. https://my-api-gateway.my-domain.com
. When the domain is added Approov will also fetch the certificate public key pin for the domain.
When the app launches it requests an updated config from Approov which contains the public key pins for the pinned domains. When a request is made to the API gateway the mobile client code will fetch the pins from the Approov SDK and validate the gateway server's certificate's public key information against the pins. If the validation succeeds the request will proceed, otherwise it will fail.
Here is a sample implementation of pinning a URLSession
from the Approov documentation:
func urlSession(_ session: URLSession,
didReceive challenge: URLAuthenticationChallenge,
completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
let protectionSpace = challenge.protectionSpace
// only handle requests that are related to server trust
if protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust {
// determine the host whose connection is pinned (this requires that the session delegate keeps a reference
// to the URLSessionTask when the task is created)
guard let urlSessionTask = sessionTask,
let host = urlSessionTaskHost(urlSessionTask: urlSessionTask),
let pins = Approov.getPins("public-key-sha256")?[host]
else {
completionHandler(.cancelAuthenticationChallenge, nil);
return
}
// check the validity of the server trust
let sslPolicy = SecPolicyCreateSSL(true, host as CFString)
var secResult = SecTrustResultType.invalid
guard let serverTrust = protectionSpace.serverTrust,
// Ensure a sensible SSL policy is used when evaluating the server trust
SecTrustSetPolicies(serverTrust, sslPolicy) == errSecSuccess,
SecTrustEvaluate(serverTrust, &secResult) == errSecSuccess,
secResult == .unspecified || secResult == .proceed
else {
completionHandler(.cancelAuthenticationChallenge, nil);
return
}
// remember the hashes of all public key infos for logging in case of pinning failure
var spkiHashesBase64 = [String](repeating: "", count: SecTrustGetCertificateCount(serverTrust))
// check public key hash of all certificates in the chain, leaf certificate first
for i in 0 ..< SecTrustGetCertificateCount(serverTrust) {
guard let serverCert = SecTrustGetCertificateAtIndex(serverTrust, i),
let spkiHashBase64 = publicKeyInfoSHA256Base64(certificate: serverCert)
else {
continue
}
spkiHashesBase64[i] = spkiHashBase64
NSLog("URLSessionDelegate: host %@ public key hash %@", host, spkiHashesBase64[i])
// check that the hash is the same as at least one of the pins
for pin in pins {
if spkiHashesBase64[i].elementsEqual(pin) {
NSLog("URLSessionDelegate: pinning valid")
completionHandler(.useCredential, URLCredential(trust: serverTrust))
// Successful match
return
}
}
}
// the certificates did not match any of the pins
completionHandler(.cancelAuthenticationChallenge, nil);
// log the hashes of all certificates in the certificate chain for the host - this helps with choosing the
// correct hash(es) to put into the Approov configuration
for i in 0 ..< SecTrustGetCertificateCount(serverTrust) {
NSLog("URLSessionDelegate: pinning invalid for host %@ and certificate %d's public key info hash %@",
host, i, spkiHashesBase64[i])
}
}
}
Hmmm what's that code doing??
- The urlSession(_:didReceive:completionHandler:) method will be called when the session sets up a connection to a remote server that uses SSL or TLS, to allow your app to verify the server’s certificate chain
- The call to
Approov.getPins("public-key-sha256")?[host]
is where we get the pins for thathost
e.g.https://my-api-gateway.my-domain.com
that were supplied in the dynamic config - Then it iterates through the certificates presented by the server checking for at least one match to a pin:
if spkiHashesBase64[i].elementsEqual(pin)
- If there's a match, the completion handler will be called with
.useCredential
, passing in the credential, and the connection will proceed 👍 - If there's no match, the connection will be cancelled ⛔️
Dynamic pinning in action
To test pinning you can use Charles to proxy the device's SSL connection to the pinned API endpoint e.g. https://my-api-gateway.my-domain.com
.
Try to call an API on https://my-api-gateway.my-domain.com
that returns a secret - such as a Login API that returns an OAuth token - and observe the secret in the API response – you can't because the API request gets cancelled client-side! That's because Charles proxy has not presented a server certificate that matches our pin.
Now disable SSL proxying in Charles and re-try. Log in succeeds!
Note that the pinning implementation is in the app code not the Approov SDK code so it's possible to do non-dynamic pinning without Approov but pin management would be far more error prone and the pins would have to be embedded and thus could be extracted from the app by a bad person.
Apps could also do dynamic pinning without Approov but they would need to host a backend service that could manage the domains and certificates and supply the updated pins to the app - so it's not straightforward to implement.
A final note on Approov: even if pinning has been implemented by an app it's possible to disable it on a jailbroken device - an exercise left to the reader - but Approov has protection against that attack and in that situation it will issue an invalid token. Which I think is...
Top comments (2)
developer.apple.com/news/?id=2sngpulc
Apple have added an attestation API to their DeviceCheck framework.
Approov featured this blog post on their own site blog.approov.io/approov-dynamic-pi... - as well as giving their own take on dynamic pinning it also goes into some detail about the differences between the Approov service and Android SafetyNet.