Skip to content

MASTG-DEMO-0148: Missing Certificate Pinning in ATS

Download MASTG-DEMO-0148 IPA Open MASTG-DEMO-0148 Folder Build MASTG-DEMO-0148 IPA

Sample

The sample below shows an app that makes HTTPS connections to three domains via URLSession.

  • sha256.badssl.com and rsa2048.badssl.com, which are pinned through ATS NSPinnedDomains.
  • example.com, which is not pinned.

In this demo, example.com represents the app's own backend. In a real app, this would be the actual first-party domain used for core functionality, such as api.myapp.com.

This distinction is central to the evaluation. Certificate pinning should be assessed for domains that belong to the app and are controlled by its developer, such as first-party backend or server-side API domains. Domains outside the developer's control should not be pinned, because their certificates and key rotation are managed by someone else.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
import Foundation

struct MastgTest {
    // SUMMARY: This sample makes HTTPS connections to three first-party domains. Two of them
    // (sha256.badssl.com and rsa2048.badssl.com) are pinned through ATS NSPinnedDomains, while the
    // app's own backend (example.com) is not pinned at all. The connections themselves always succeed;
    // whether each domain is actually protected depends on the NSPinnedDomains configuration in Info.plist.

    // PASS: [MASTG-TEST-0385] A first-party domain that is pinned via NSPinnedDomains.
    static let pinnedEndpoint = "https://sha256.badssl.com/"

    // PASS: [MASTG-TEST-0385] Another first-party domain that is pinned via NSPinnedDomains.
    static let secondPinnedEndpoint = "https://rsa2048.badssl.com/"

    // FAIL: [MASTG-TEST-0385] The app's own backend. It is a relevant first-party domain that should be pinned but isn't.
    static let developerEndpoint = "https://example.com/"

    static func mastgTest(completion: @escaping (String) -> Void) {
        var result = "Testing HTTPS connections for ATS certificate pinning:\n\n"
        let group = DispatchGroup()

        for endpoint in [pinnedEndpoint, secondPinnedEndpoint, developerEndpoint] {
            guard let url = URL(string: endpoint) else {
                result += "Invalid URL: \(endpoint)\n"
                continue
            }

            group.enter()
            URLSession.shared.dataTask(with: url) { _, response, error in
                if let error = error as NSError? {
                    result += "\(endpoint) failed: \(error.localizedDescription)\n"
                } else if let httpResponse = response as? HTTPURLResponse {
                    result += "\(endpoint) returned status: \(httpResponse.statusCode)\n"
                } else {
                    result += "\(endpoint) completed without HTTP response.\n"
                }
                group.leave()
            }.resume()
        }

        group.notify(queue: .main) {
            completion(result)
        }
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<?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>NSPinnedDomains</key>
            <dict>
                <key>sha256.badssl.com</key>
                <dict>
                    <key>NSIncludesSubdomains</key>
                    <true/>
                    <key>NSPinnedCAIdentities</key>
                    <array>
                        <dict>
                            <key>SPKI-SHA256-BASE64</key>
                            <string>C5+lpZ7tcVwmwQIMcRtPbsQtWLABXhQzejna0wHFr8M=</string>
                        </dict>
                    </array>
                </dict>
                <key>rsa2048.badssl.com</key>
                <dict>
                    <key>NSIncludesSubdomains</key>
                    <true/>
                    <key>NSPinnedCAIdentities</key>
                    <array>
                        <dict>
                            <key>SPKI-SHA256-BASE64</key>
                            <string>C5+lpZ7tcVwmwQIMcRtPbsQtWLABXhQzejna0wHFr8M=</string>
                        </dict>
                    </array>
                </dict>
                <!-- example.com (the app's own backend) is NOT pinned: this is the missing pin -->
            </dict>
        </dict>
    </dict>
</plist>

Steps

  1. Extract the app ( Exploring the App Package) 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 ( Convert Plist Files to JSON).
  3. Extract the NSPinnedDomains configuration from NSAppTransportSecurity.
  4. Use rabin2 to extract HTTP URLs from the app binary and compare them against the pinned domains.
run.sh
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
#!/bin/bash

plutil -convert xml1 -o Info_reversed.plist Info_reversed.plist
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

jq '.NSAppTransportSecurity.NSPinnedDomains' Info_reversed.json > ats_pinned_domains.json

rabin2 -z MASTestApp | grep http > output.txt

Observation

The output shows the whole NSPinnedDomains configuration:

ats_pinned_domains.json
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
{
  "sha256.badssl.com": {
    "NSPinnedCAIdentities": [
      {
        "SPKI-SHA256-BASE64": "C5+lpZ7tcVwmwQIMcRtPbsQtWLABXhQzejna0wHFr8M="
      }
    ],
    "NSIncludesSubdomains": true
  },
  "rsa2048.badssl.com": {
    "NSPinnedCAIdentities": [
      {
        "SPKI-SHA256-BASE64": "C5+lpZ7tcVwmwQIMcRtPbsQtWLABXhQzejna0wHFr8M="
      }
    ],
    "NSIncludesSubdomains": true
  }
}

The output from rabin2 shows that the app contains hardcoded URLs for all three domains:

output.txt
1
2
3
0   0x000153c0 0x000153c0 26  27   3.__TEXT.__cstring         ascii https://sha256.badssl.com/
1   0x000153e0 0x000153e0 27  28   3.__TEXT.__cstring         ascii https://rsa2048.badssl.com/
2   0x00015400 0x00015400 20  21   3.__TEXT.__cstring         ascii https://example.com/

Evaluation

The test case fails because the app connects to its own backend, represented in this demo by example.com, but that domain has no entry under NSPinnedDomains.

Only sha256.badssl.com and rsa2048.badssl.com are pinned. As a result, connections to the app-controlled backend rely only on the system CA trust store and are not protected by ATS certificate pinning against a MITM attacker who can cause the device to trust a forged or misissued certificate.