What is App Transport Security, and how do you implement certificate pinning?
TL;DR
App Transport Security (ATS) is on by default. It blocks plain http:// and weak TLS. Connections need HTTPS with TLS 1.2+, forward secrecy and a valid certificate.
Certificate pinning means "trust only my server's key, not every certificate authority". It protects against a fake certificate on a compromised or corporate network.
Pin the public key (not the whole certificate), and always ship a backup pin.
ATS basics
Without any setting, this fails:
let url = URL(string: "http://api.example.com/data")! // plain HTTP
let (data, _) = try await URLSession.shared.data(from: url) // error: ATS blocked itYou can add exceptions in Info.plist:
<key>NSAppTransportSecurity</key>
<dict>
<key>NSExceptionDomains</key>
<dict>
<key>legacy.example.com</key>
<dict>
<key>NSExceptionAllowsInsecureHTTPLoads</key>
<true/>
</dict>
</dict>
</dict>NSAllowsArbitraryLoads = YESturns ATS off everywhere. App Review may ask you to justify it. Avoid it.NSAllowsLocalNetworkingallows local addresses, useful for development.NSAllowsArbitraryLoadsInWebContentrelaxes ATS only for web views.
Prefer narrow, per-domain exceptions, and remove them when the server is fixed.
Why pinning?
Normal TLS trusts any certificate signed by any trusted certificate authority (CA). If an attacker installs their own CA on the device, or a CA is compromised, they can read your traffic. Pinning says: "Even with a valid chain, the server must present this key."
Option 1: declarative pinning (iOS 14+)
<key>NSAppTransportSecurity</key>
<dict>
<key>NSPinnedDomains</key>
<dict>
<key>api.example.com</key>
<dict>
<key>NSIncludesSubdomains</key>
<true/>
<key>NSPinnedLeafIdentities</key>
<array>
<dict>
<key>SPKI-SHA256-BASE64</key>
<string>primaryKeyHashBase64=</string>
</dict>
<dict>
<key>SPKI-SHA256-BASE64</key>
<string>backupKeyHashBase64=</string>
</dict>
</array>
</dict>
</dict>
</dict>No code. Works for URLSession and other system networking. You can pin the leaf key (NSPinnedLeafIdentities) or a CA key (NSPinnedCAIdentities).
Option 2: pinning in code
import CryptoKit
final class PinningDelegate: NSObject, URLSessionDelegate {
private let pinnedHashes: Set<String> = ["primaryKeyHashBase64=", "backupKeyHashBase64="]
func urlSession(_ session: URLSession,
didReceive challenge: URLAuthenticationChallenge)
async -> (URLSession.AuthChallengeDisposition, URLCredential?) {
guard challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust,
let trust = challenge.protectionSpace.serverTrust,
SecTrustEvaluateWithError(trust, nil), // 1. normal validation first
let chain = SecTrustCopyCertificateChain(trust) as? [SecCertificate],
let leaf = chain.first,
let key = SecCertificateCopyKey(leaf),
let keyData = SecKeyCopyExternalRepresentation(key, nil) as Data?
else {
return (.cancelAuthenticationChallenge, nil)
}
// 2. Hash the key and compare with our pins.
// Note: a real SPKI hash also includes the ASN.1 header for the key type.
let hash = Data(SHA256.hash(data: keyData)).base64EncodedString()
guard pinnedHashes.contains(hash) else {
return (.cancelAuthenticationChallenge, nil)
}
return (.useCredential, URLCredential(trust: trust))
}
}
let session = URLSession(configuration: .default, delegate: PinningDelegate(), delegateQueue: nil)Always do the normal trust check first. Pinning adds a check. It does not replace one.
What to pin
| Pin | Pros | Cons |
|---|---|---|
| Leaf certificate | Simple | Breaks every time the certificate renews |
| Leaf public key (SPKI) | Survives renewal if the key is reused | Must plan key rotation |
| Intermediate or CA key | Rarely changes | Trusts more certificates |
The big risk: bricking your app
If the server key changes and the app has no matching pin, every request fails until users update. Protect yourself:
- Always include at least one backup pin for a key you keep offline.
- Plan rotation: ship the new pin in an app update before you change the server key.
- Have a way to monitor failures, like logging pin errors to analytics.
Common follow-up questions
- Does pinning stop a determined attacker? No. On a jailbroken device it can be bypassed. It protects users on hostile networks.
- How do you debug with Charles or Proxyman when pinning is on? Turn pinning off in debug builds only, with
#if DEBUGor a separate configuration. - Do you still need ATS if you pin? Yes. ATS sets the baseline; pinning narrows trust further.
- Should every app pin? Not always. It adds operational risk. It makes most sense for banking, health and high-value APIs.