Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions best-practices/MASTG-BEST-0032.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
---
title: Implement Certificate Pinning on iOS
alias: ios-implement-certificate-pinning
id: MASTG-BEST-0032
platform: ios
knowledge: [MASTG-KNOW-0072]
---

## Overview

Certificate pinning lets an iOS app reject TLS connections to servers that don't present a specific expected certificate or public key, even if the server's certificate is signed by a CA trusted by the OS. This protects against MITM attacks by rogue or compromised certificate authorities.

## Recommendation

Use Apple's built-in [Identity Pinning via `NSPinnedDomains`](https://developer.apple.com/news/?id=g9ejcf8y) in `Info.plist` as the primary pinning mechanism. This is the simplest and most maintainable approach, as it requires no code changes and is automatically enforced by the system for all connections made through the URL Loading System.

```xml
<key>NSAppTransportSecurity</key>
<dict>
<key>NSPinnedDomains</key>
<dict>
<key>example.com</key>
<dict>
<key>NSIncludesSubdomains</key>
<true/>
<key>NSPinnedCAIdentities</key>
<array>
<dict>
<key>SPKI-SHA256-BASE64</key>
<string>+[BASE64-ENCODED SHA-256 HASH OF SUBJECT PUBLIC KEY INFO]</string>
</dict>
</array>
</dict>
</dict>
</dict>
```

If you need more control, implement pinning in the [`URLSessionDelegate`](https://developer.apple.com/documentation/foundation/urlsessiondelegate) method [`urlSession(_:didReceive:completionHandler:)`](https://developer.apple.com/documentation/foundation/urlsessiondelegate/1409308-urlsession) and perform [manual server trust authentication](https://developer.apple.com/documentation/foundation/url_loading_system/handling_an_authentication_challenge/performing_manual_server_trust_authentication).

## Caveats and Considerations

- **Pin to a CA public key, not a leaf certificate** when possible. Leaf certificates rotate frequently; CA public keys are more stable, avoiding forced app updates on every certificate renewal.
- **Always include a backup pin** (a second CA or leaf key) to ensure connectivity if the primary certificate is replaced unexpectedly. Without a backup, a certificate rotation could render the app unable to connect.
- **Manage pin rotation carefully.** Unlike Android's Network Security Configuration, `NSPinnedDomains` doesn't support expiration dates. Plan certificate rotation before pins become stale.
- **Pinning doesn't replace proper TLS configuration.** Ensure the server is using strong cipher suites, up-to-date TLS versions, and valid certificates from trusted CAs.
- **Pinning can be bypassed** on jailbroken devices or via tools such as [SSL Kill Switch 2](https://github.com/nabla-c0d3/ssl-kill-switch2), and by reverse-engineering and repackaging the app. Complement pinning with other controls such as jailbreak detection and app integrity checks for high-risk scenarios.
- **Cross-platform and third-party frameworks** may use their own network stacks that bypass ATS and `NSPinnedDomains` entirely. Verify that pinning is enforced at the framework level (for example, Dart's `HttpClient` for Flutter).

## References

- Apple Developer Documentation: [Identity Pinning: How to configure server certificates for your app](https://developer.apple.com/news/?id=g9ejcf8y)
- Apple Developer Documentation: [NSPinnedDomains](https://developer.apple.com/documentation/bundleresources/information-property-list/nsapptransportsecurity/nspinneddomains)
- Apple Developer Documentation: [Performing Manual Server Trust Authentication](https://developer.apple.com/documentation/foundation/url_loading_system/handling_an_authentication_challenge/performing_manual_server_trust_authentication)
- Apple Developer Documentation: [Preventing Insecure Network Connections](https://developer.apple.com/documentation/security/preventing_insecure_network_connections)
10 changes: 10 additions & 0 deletions demos/ios/MASVS-NETWORK/MASTG-DEMO-0x01/Info.plist
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSAppTransportSecurity</key>
<dict>
<!-- No NSPinnedDomains configured: certificate pinning is absent -->
</dict>
</dict>
</plist>
86 changes: 86 additions & 0 deletions demos/ios/MASVS-NETWORK/MASTG-DEMO-0x01/Info_reversed.plist
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>BuildMachineOSBuild</key>
<string>25C56</string>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>MASTestApp</string>
<key>CFBundleIdentifier</key>
<string>org.owasp.mastestapp.MASTestApp-iOS</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>MASTestApp</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleSupportedPlatforms</key>
<array>
<string>iPhoneOS</string>
</array>
<key>CFBundleVersion</key>
<string>1</string>
<key>DTCompiler</key>
<string>com.apple.compilers.llvm.clang.1_0</string>
<key>DTPlatformBuild</key>
<string>23C53</string>
<key>DTPlatformName</key>
<string>iphoneos</string>
<key>DTPlatformVersion</key>
<string>26.2</string>
<key>DTSDKBuild</key>
<string>23C53</string>
<key>DTSDKName</key>
<string>iphoneos26.2</string>
<key>DTXcode</key>
<string>2620</string>
<key>DTXcodeBuild</key>
<string>17C52</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>MinimumOSVersion</key>
<string>17.1.1</string>
<key>NSAppTransportSecurity</key>
<dict/>
<key>UIApplicationSceneManifest</key>
<dict>
<key>UIApplicationSupportsMultipleScenes</key>
<true/>
<key>UISceneConfigurations</key>
<dict/>
</dict>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>UIDeviceFamily</key>
<array>
<integer>1</integer>
<integer>2</integer>
</array>
<key>UILaunchScreen</key>
<dict>
<key>UILaunchScreen</key>
<dict/>
</dict>
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>arm64</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~iphone</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
</dict>
</plist>
32 changes: 32 additions & 0 deletions demos/ios/MASVS-NETWORK/MASTG-DEMO-0x01/MASTG-DEMO-0x01.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
---
platform: ios
title: Missing Certificate Pinning in ATS
code: [swift, xml]
id: MASTG-DEMO-0x01
test: MASTG-TEST-0x01
kind: fail
---

## Sample

The sample below shows an app that makes HTTPS connections to `api.example.com` via `URLSession`. While the app uses HTTPS, the `Info.plist` file contains an `NSAppTransportSecurity` section with no `NSPinnedDomains` key, meaning no certificate pinning is configured via ATS:

{{ MastgTest.swift # Info.plist }}

## Steps

1. Extract the app (@MASTG-TECH-0058) and locate the `Info.plist` file inside the app bundle (which we'll name `Info_reversed.plist`).
2. Convert the `Info.plist` to a JSON format (@MASTG-TECH-0138).
3. Search for `NSPinnedDomains` in the ATS configuration.

{{ run.sh }}

## Observation

The output is empty, indicating that `NSPinnedDomains` is not present in the `NSAppTransportSecurity` section of the `Info.plist`:

{{ output.txt }}

## Evaluation

The test fails because the `Info.plist` does not contain a `NSPinnedDomains` key inside `NSAppTransportSecurity`. As a result, the app doesn't enforce certificate pinning via ATS and relies entirely on the system CA trust store.
21 changes: 21 additions & 0 deletions demos/ios/MASVS-NETWORK/MASTG-DEMO-0x01/MastgTest.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import Foundation

struct MastgTest {
// SUMMARY: This sample demonstrates missing certificate pinning configuration in ATS.
// The app makes HTTPS connections but the Info.plist does not configure NSPinnedDomains,
// leaving it vulnerable to MITM attacks via rogue or compromised certificate authorities.

static func mastgTest(completion: @escaping (String) -> Void) {
// FAIL: [MASTG-TEST-0x01] No certificate pinning configured via NSPinnedDomains in Info.plist.
// The app relies solely on the system CA trust store.
let url = URL(string: "https://api.example.com/data")!
let task = URLSession.shared.dataTask(with: url) { _, response, error in
if let error = error {
completion("Request failed: \(error.localizedDescription)")
} else if let httpResponse = response as? HTTPURLResponse {
completion("Request to api.example.com returned status: \(httpResponse.statusCode)")
}
}
task.resume()
}
}
Empty file.
8 changes: 8 additions & 0 deletions demos/ios/MASVS-NETWORK/MASTG-DEMO-0x01/run.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
#!/bin/bash

plutil -convert json -o Info_reversed.json Info_reversed.plist

# pretty print json
jq . Info_reversed.json > Info_reversed.json.tmp && mv Info_reversed.json.tmp Info_reversed.json

gron -m Info_reversed.json | grep 'NSPinnedDomains' > output.txt
28 changes: 28 additions & 0 deletions demos/ios/MASVS-NETWORK/MASTG-DEMO-0x02/Info.plist
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSAppTransportSecurity</key>
<dict/>
<key>TSKConfiguration</key>
<dict>
<key>TSKSwizzleNetworkDelegates</key>
<true />
<key>TSKPinnedDomains</key>
<dict>
<key>api.example.com</key>
<dict>
<key>TSKIncludeSubdomains</key>
<true />
<key>TSKPublicKeyHashes</key>
<array>
<string>HXXQgxueCIU5TTLHob/bPbwcKOKw6DkfsTWYHbxbqTY=</string>
<string>0SDf3cRToyZJaMsoS17oF72VMavLxj/N7WkTpymPxdc=</string>
</array>
<key>TSKExpirationDate</key>
<string>2020-01-01</string>
</dict>
</dict>
</dict>
</dict>
</plist>
106 changes: 106 additions & 0 deletions demos/ios/MASVS-NETWORK/MASTG-DEMO-0x02/Info_reversed.plist
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>BuildMachineOSBuild</key>
<string>25C56</string>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>MASTestApp</string>
<key>CFBundleIdentifier</key>
<string>org.owasp.mastestapp.MASTestApp-iOS</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>MASTestApp</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleSupportedPlatforms</key>
<array>
<string>iPhoneOS</string>
</array>
<key>CFBundleVersion</key>
<string>1</string>
<key>DTCompiler</key>
<string>com.apple.compilers.llvm.clang.1_0</string>
<key>DTPlatformBuild</key>
<string>23C53</string>
<key>DTPlatformName</key>
<string>iphoneos</string>
<key>DTPlatformVersion</key>
<string>26.2</string>
<key>DTSDKBuild</key>
<string>23C53</string>
<key>DTSDKName</key>
<string>iphoneos26.2</string>
<key>DTXcode</key>
<string>2620</string>
<key>DTXcodeBuild</key>
<string>17C52</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>MinimumOSVersion</key>
<string>17.1.1</string>
<key>NSAppTransportSecurity</key>
<dict/>
<key>TSKConfiguration</key>
<dict>
<key>TSKPinnedDomains</key>
<dict>
<key>api.example.com</key>
<dict>
<key>TSKExpirationDate</key>
<string>2020-01-01</string>
<key>TSKIncludeSubdomains</key>
<true/>
<key>TSKPublicKeyHashes</key>
<array>
<string>HXXQgxueCIU5TTLHob/bPbwcKOKw6DkfsTWYHbxbqTY=</string>
<string>0SDf3cRToyZJaMsoS17oF72VMavLxj/N7WkTpymPxdc=</string>
</array>
</dict>
</dict>
<key>TSKSwizzleNetworkDelegates</key>
<true/>
</dict>
<key>UIApplicationSceneManifest</key>
<dict>
<key>UIApplicationSupportsMultipleScenes</key>
<true/>
<key>UISceneConfigurations</key>
<dict/>
</dict>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>UIDeviceFamily</key>
<array>
<integer>1</integer>
<integer>2</integer>
</array>
<key>UILaunchScreen</key>
<dict>
<key>UILaunchScreen</key>
<dict/>
</dict>
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>arm64</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~iphone</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
</dict>
</plist>
32 changes: 32 additions & 0 deletions demos/ios/MASVS-NETWORK/MASTG-DEMO-0x02/MASTG-DEMO-0x02.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
---
platform: ios
title: Expired Certificate Pins in ATS
code: [swift, xml]
id: MASTG-DEMO-0x02
test: MASTG-TEST-0x02
kind: fail
---

## Sample

The sample below shows an app that makes HTTPS connections to `api.example.com`. It uses [TrustKit](https://github.com/datatheorem/TrustKit) for certificate pinning, configured via the `TSKConfiguration` key in `Info.plist`. However, the `TSKExpirationDate` for the pinned domain is set to `2020-01-01`, which is in the past. After this date, TrustKit stops enforcing certificate pinning for the domain and falls back to the system CA trust store:

{{ MastgTest.swift # Info.plist }}

## Steps

1. Extract the app (@MASTG-TECH-0058) and locate the `Info.plist` file inside the app bundle (which we'll name `Info_reversed.plist`).
2. Convert the `Info.plist` to a JSON format (@MASTG-TECH-0138).
3. Search for `TSKExpirationDate` values in the TrustKit configuration.

{{ run.sh }}

## Observation

The output shows the `TSKExpirationDate` value for the pinned domain:

{{ output.txt }}

## Evaluation

The test fails because the `TSKExpirationDate` for `api.example.com` is `2020-01-01`, which is in the past. TrustKit has already stopped enforcing certificate pinning for this domain.
Loading