Mutual TLS (mTLS) is the strongest device-authentication primitive available to a mobile app: the server doesn’t just trust a bearer token, it cryptographically verifies that the client holds a private key it issued specifically to this device. The threat-model upgrade is huge, stolen credentials become unusable on any device but the one they were issued to.
Both iOS and Android support mTLS. Both platforms have hardware-backed key storage (Secure Enclave on iPhone, KeyStore on Android with TEE/StrongBox where available). The PayloadGuard team gets the same question from architects every month: “Should we use mTLS on both? What’s the threat-model delta?” This post is the engineering answer.
What mTLS actually proves
When the client opens a TLS connection, the server sends its certificate (as in regular TLS). In mTLS, the server also sends a CertificateRequest, and the client responds with its own certificate plus a CertificateVerify signed by the client’s private key. The server validates:
- The certificate chain (signed by a CA the server trusts).
- The
CertificateVerifysignature (proves the client holds the private key, not just a copy of the certificate).
The strength of mTLS hinges on the private key’s storage. If the key is on disk, an attacker who roots the device or extracts the keychain backup can forge the CertificateVerify from a different device. If the key is in hardware that doesn’t release it, the attacker cannot extract it, they can only operate on the original device, and only while it’s compromised.
This is where iOS and Android diverge.
iOS: Secure Enclave + SecIdentity
On iOS, the client key for mTLS lives in the Secure Enclave (SE). Generated with:
import CryptoKit
import Security
let access = SecAccessControlCreateWithFlags(
nil,
kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
.privateKeyUsage,
nil
)!
let attributes: [String: Any] = [
kSecAttrKeyType as String: kSecAttrKeyTypeECSECPrimeRandom,
kSecAttrKeySizeInBits as String: 256,
kSecAttrTokenID as String: kSecAttrTokenIDSecureEnclave,
kSecPrivateKeyAttrs as String: [
kSecAttrIsPermanent as String: true,
kSecAttrApplicationTag as String: "com.app.mtls.client".data(using: .utf8)!,
kSecAttrAccessControl as String: access
]
]
var error: Unmanaged<CFError>?
let privateKey = SecKeyCreateRandomKey(attributes as CFDictionary, &error)!
kSecAttrTokenIDSecureEnclave is the critical line. The private key now physically lives in the SE, a separate chip with its own ROM, its own RAM, its own RSA accelerator. The SecKey Swift handle is a reference; signing operations marshal the data to the SE, which signs it and returns the signature. The key bits never enter the application processor’s memory.
For mTLS, the client signs the TLS CertificateVerify using SecKeyCreateSignature. iOS’s URLSessionDelegate hands you the challenge:
func urlSession(
_ session: URLSession,
didReceive challenge: URLAuthenticationChallenge,
completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void
) {
if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodClientCertificate {
let identity: SecIdentity = loadIdentityFromSE() // wraps SE private key + cert
let credential = URLCredential(identity: identity, certificates: nil, persistence: .none)
completionHandler(.useCredential, credential)
}
}
Because the SecIdentity references the SE-resident key, the CertificateVerify is produced by the SE. An attacker who jailbreaks the device can issue new signatures from that device (the SE will still serve operations to the running process), but they cannot copy the key off the device to a workstation or a different phone.
This is the threat-model property that matters: stolen credentials cannot be cloned. Even with full jailbreak access, the attacker is bounded to that specific device, and the device’s compromise can be detected and revoked.
Android: KeyStore, with caveats
Android’s analog is the AndroidKeyStore provider with setUserAuthenticationRequired(true) and, on supporting hardware, StrongBoxBacked. The intent is the same, a key that cannot leave the secure environment, but the engineering reality is more uneven:
-
StrongBox is optional. Pixel 3+ and a subset of Samsung devices have a dedicated security chip (StrongBox). Most other Android devices use the TEE (TrustZone), which is software running in a privileged mode on the same application processor. The boundary is logical, not physical.
-
Vendor implementation quality varies. The Android Compatibility Suite tests for interface compliance, not for implementation robustness. CVE history shows numerous TEE-implementation bugs across vendors (Samsung S-Boot, Qualcomm QSEE, MediaTek), some leading to key extraction.
-
Hardware-attestation is the only signal. On Android you can query
KeyInfo.isInsideSecureHardware(), but on devices without StrongBox, you’re trusting the TEE attestation, which is itself signed by a vendor key that has occasionally leaked (Samsung’s 2023 ANDROID-PLATFORM key incident is the canonical example). -
mTLS plumbing is awkward. Android’s
KeyManagerandX509KeyManagerinterfaces predate the modern KeyStore by years. Wiring a KeyStore-backed private key into OkHttp or HttpURLConnection requires customKeyManagerimplementations that handle the asynchronous signing operation.
The result: on a current Pixel with StrongBox, Android’s mTLS story is roughly comparable to iOS’s. On most other Android devices, the threat model is weaker, the TEE is software on a busy processor, and the vendor-specific attestation chain is the trust root.
iOS’s Secure Enclave is the same chip on every device since the iPhone 5s (with generation-over-generation improvements). The PayloadGuard team’s empirical observation: iOS’s floor is higher than Android’s floor, and the iOS implementation surface is one Apple stack, not N vendor stacks.
What this means for PayloadGuard’s pin walk
PayloadGuard does pinned mTLS by default. The server’s certificate chain is pinned via SPKI hashes (see TLS pinning with certificate rotation), and the client uses a Secure-Enclave-backed identity for the CertificateVerify. The combination resists three categories of attack:
- MITM via custom CA: blocked by SPKI pinning on the server cert chain.
- Stolen credentials replay from another device: blocked because the client key never left this device.
- Stolen credentials replay on the same device after jailbreak: still possible during the compromise window, but visible to RuntimeGuard SDK’s jailbreak detector and revokable from the server side via cert revocation.
The threat that survives this stack: a rooted device, an active attacker session, a signed CertificateVerify produced by the SE while the attacker is logged in. That is the residual risk, and it’s bounded to the device + the attack window, not extensible across the user’s fleet. The full breakdown of what this stack covers and what it defers lives in the PayloadGuard threat model.
Where DeviceCheck and App Attest fit
The mTLS identity proves “this is a key I issued to this device.” DeviceCheck (the legacy API) and App Attest (the iOS 14+ API) prove “this binary is the binary I signed, running on real Apple hardware.” They are complementary, not substitutes:
- mTLS: device-bound key, server-issued, app-controlled, revokable.
- App Attest: binary-bound attestation, Apple-issued, used to detect tampered apps before issuing the mTLS cert in the first place.
The PayloadGuard pattern is: app launches → App Attest to prove “I’m a real app on a real device” → server-side enrollment endpoint issues an mTLS certificate after attestation → every subsequent request uses mTLS. Each layer’s compromise alone doesn’t break the chain; you need both.
When to skip mTLS
mTLS is the right answer when your threat model includes credential theft and your users’ accounts hold meaningful value. It is the wrong answer for read-only public data or for apps where the cost of mTLS issuance (a server-side enrollment endpoint, cert lifecycle management, revocation flows) exceeds the credential-replay risk.
For most fintech, healthcare, enterprise, and crypto-wallet apps: mTLS is the right answer, and the iOS Secure Enclave implementation gets you closer to a clean security argument than the Android equivalent.
See /sdk/payloadguard for the marketing summary, /docs/payloadguard for the mTLS configuration reference, and the companion posts on TLS pinning and App Attest as device identity.

Comments
Nothing yet. Say the first thing.
Sign in to join the conversation.