SSL Pinning: Article "Identity Pinning: How to configure server certificates for your app"

Hi,

The article https://developer.apple.com/news/?id=g9ejcf8y "Identity Pinning: How to configure server certificates for your app" from 2021-01-14 looks really promising.
However, I cannot get the settings to have any effect on iOS 14.3 (Xcode 12.3).
I tested with NSPinnedCAIdentities and Charles Proxy as well as an incorrect RootCA key. In both cases the requests were successful, Charles was able to decrypt the traffic.

Questions that come to my mind:
  • Where is the detailed documentation of the keys?

  • Which platforms/versions are supported?


Thanks and kind regards,
Lars



Where is the detailed documentation of the keys?

There isn’t any formal documentation for this )-: although we do already have a bug tracking that requirement (r. 60639312).

Which platforms/versions are supported?

iOS 14 and friends.

What API are you using? NSURLSession?

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@apple.com"

However, I cannot get the settings to have any effect on iOS 14.3 (Xcode 12.3).

Using example.org with URLSession, I would suggest a few things while testing this functionality:

1) When you make changes to your info.plist, it's good to delete the app and test with a fresh install. Just to make sure the evaluation is not running into TLS caching each time. For example, with example.org I tested the following on a fresh install and all was good:
Code Block xml
<key>NSIncludesSubdomains</key>
<true/>
<key>NSPinnedCAIdentities</key>
<array>
<dict>
<key>SPKI-SHA256-BASE64</key>
<string>r/mIkG3eEpVdm+u/ko/cwxzOMo1bk4TyHIlByibiA5E=</string>
</dict>
</array>

I then altered the SPKI-SHA256-BASE64 to be the following and received a positive result:
Code Block xml
<key>NSIncludesSubdomains</key>
<true/>
<key>NSPinnedCAIdentities</key>
<array>
<dict>
<key>SPKI-SHA256-BASE64</key>
<string>r/333mIkG3eEpVdm+u/ko/cwxzOMo1bk4TyHIlByibiA5E=</string>
</dict>
</array>

This is most likely due to TLS caching, as described here, but it can be confusing.

Looking at another example like www.apple.com. you can get a PEM copy of the CA certificate in the chain by using something like: % openssl s_client -showcerts -connect www.apple.com:443. Note that I am using www.apple.com:443 in this example because you may run into a situation where images.apple.com is given to you if you use apple.com:443.

In the output from openssl s_client you should be able to save the PEM encoded form of the root certificate. From there, run the command provided in the article to make a public key hash output of this certificate:

Code Block text
% cat appleca.pem | openssl x509 -inform pem -noout -outform pem -pubkey | openssl pkey -pubin -inform pem -outform der | openssl dgst -sha256 -binary | openssl enc -base64
lwTPN61Qg5+1qAU+Mik9sFaDX5hLo2AHP80YR+IgN6M=


From there add pin this as a NSPinnedCAIdentities against:

Code Block xml
<key>NSAppTransportSecurity</key>
<dict>
<key>NSPinnedDomains</key>
<dict>
<key>apple.com</key>
<dict>
<key>NSIncludesSubdomains</key>
<true/>
<key>NSPinnedCAIdentities</key>
<array>
<dict>
<key>SPKI-SHA256-BASE64</key>
<string>lwTPN61Qg5+1qAU+Mik9sFaDX5hLo2AHP80YR+IgN6M=</string>
</dict>
</array>
</dict>
</dict>
</dict>


In your app then use the follow URLSession example with apple.com:

Code Block swift
var config: URLSessionConfiguration = URLSessionConfiguration.default
var urlSession = URLSession(configuration: config, delegate: self, delegateQueue: .main)
...
guard let url = URL(string: "https://apple.com") else { return }
var urlRequest = URLRequest(url: url)
urlRequest.httpMethod = "GET"
let task = urlSession?.dataTask(with: urlRequest, completionHandler: { (data, response, error) in
...
})
task?.resume()



Matt Eaton
DTS Engineering, CoreOS
meaton3@apple.com
@Matt:
Thanks! I'll try your tips.

@Quinn:
We use Alamofire (which uses URLSession) and WKWebView.
I assume that SFSafariViewController ist not affected by these settings, correct?

Since we still need to support iOS 13 (client's requirement), it'll experimental for now.

Lars
@Matt:
I was able to produce a failing connection (wrong SPKI-SHA256-BASE64) with a certificate error trying to load https://apple.com. But downloading https://www.apple.com still works – even though NSIncludesSubdomains is true.
Testing more, I found that some subdomains are pinned correctly, some are not. This is also the case for the sub domains I was originally trying to pin in my project.

Info.plist section:
Code Block
<key>NSAppTransportSecurity</key>
<dict>
<key>NSPinnedDomains</key>
<dict>
<key>apple.com</key>
<dict>
<key>NSIncludesSubdomains</key>
<true/>
<key>NSPinnedCAIdentities</key>
<array>
<dict>
<key>SPKI-SHA256-BASE64</key>
<string>r/333mIkG3eEpVdm+u/ko/cwxzOMo1bk4TyHIlByibiA5E=</string>
</dict>
</array>
</dict>
</dict>
</dict>


Code:
Code Block
class ViewController: UIViewController
{
private lazy var urlSession: URLSession =
{
URLSession(configuration: URLSessionConfiguration.default, delegate: nil, delegateQueue: .main)
}()
override func viewDidLoad()
{
super.viewDidLoad()
[
"apple.com",
"www.apple.com",
"images.apple.com",
"store.apple.com",
]
.map { URL(string: "https://\($0)")! } /* intentional crash on failure */
.forEach
{ url in
var urlRequest = URLRequest(url: url)
urlRequest.httpMethod = "GET"
let task = self.urlSession.dataTask(with: urlRequest)
{ (data, response, error) in
var text: String?
if let data = data
{
text = String(data: data, encoding: .ascii)?
.trimmingCharacters(in: .whitespacesAndNewlines)
}
let result = "\(text ?? data?.debugDescription ?? error.debugDescription)".prefix(100)
print("URL: \(url): result: \(result)")
}
task.resume()
}
}
}



Output (filtered)
Code Block
URL: https://apple.com: result: Optional(Error Domain=NSURLErrorDomain Code=-1202 "The certificate for this server is invalid. You m
URL: https://images.apple.com: result: Optional(Error Domain=NSURLErrorDomain Code=-1202 "The certificate for this server is invalid. You m
URL: https://www.apple.com: result: <!DOCTYPE html>
URL: https://store.apple.com: result: Optional(Error Domain=NSURLErrorDomain Code=-1202 "The certificate for this server is invalid. You m


Thanks
Lars

Lars,

When using NSPinnedCAIdentities pay close attention to the certificate chain when you are evaluating those four domains. This may be where you are running into issues. For example, if I hash the public key for the CA's certificate for each of those four domains you will notice that apple.com and images.apple.com are the same while www.apple.com and store.apple.com are different.

Code Block text
% openssl s_client -showcerts -connect apple.com:443
CN = images.apple.com
apple.com Public Key Hash:
uUwZgwDOxcBXrQcntwu+kYFpkiVkOaezL0WYEZ3anJc=
-----
% openssl s_client -showcerts -connect www.apple.com:443
CN = www.apple.com
www.apple.com Public Key Hash:
lwTPN61Qg5+1qAU+Mik9sFaDX5hLo2AHP80YR+IgN6M=
-----
% openssl s_client -showcerts -connect images.apple.com:443
CN = images.apple.com
images.apple.com Public Key Hash:
uUwZgwDOxcBXrQcntwu+kYFpkiVkOaezL0WYEZ3anJc=
-----
% openssl s_client -showcerts -connect store.apple.com:443
CN = store.apple.com
store.apple.com Public Key Hash:
4ZMBBKlWyhpdsKqKnbXEkuo4CnSxw6b7svlcKIJWfEA=



Matt Eaton
DTS Engineering, CoreOS
meaton3@apple.com
Hi Matt,

unless I am mistaken, that is not quite what I am looking at.

I am intentionally using the wrong key to provoke an SSL error.
r/333mIkG3eEpVdm+u/ko/cwxzOMo1bk4TyHIlByibiA5E= does not match the RootCA public key hash for www.apple.com. Why does the request succeed?

I even tried dGVzdAo= (Base64 for "test"). Same result:
Requests fail for some subdomains (expected behaviour) but I get a valid response for www.apple.com – which is unexpected.

Similar behaviour for some other domains (I don't want to post them here).

Thanks,
Lars
Lars,

I am intentionally using the wrong key to provoke an SSL error.
r/333mIkG3eEpVdm+u/ko/cwxzOMo1bk4TyHIlByibiA5E= does not match the RootCA public key hash for www.apple.com. Why does the request succeed?

The www.apple.com side does seem to be inconsistent compared to testing with apple.com. With apple.com I can test a positive and negative result successfully.

However, when using:

Code Block xml
<key>apple.com</key>
<dict>
<key>NSIncludesSubdomains</key>
<true/>
<key>NSPinnedCAIdentities</key>
<array>
<dict>
<key>SPKI-SHA256-BASE64</key>
<string>lwTPN61Qg5+1qAU+Mik9sFaDX5hLo2AHP80YR+IgN6M=</string>
</dict>
</array>
</dict>

With https://www.apple.com in my URLSessionDataTask, I always produce a positive result even if I alter the Public Key Hash. Likewise, if I use www.apple.com in both the NSPinnedDomains and in my URLSessionDataTask I cannot produce a negative result. Even if I leave the hash blank.


I think we should get a bug report down to get some further documentation on this. Please follow up with the Feedback ID.


Matt Eaton
DTS Engineering, CoreOS
meaton3@apple.com
Matt,

here you go: FB8989889

Lars
Thank you Lars, I do see your bug report internally.



Matt Eaton
DTS Engineering, CoreOS
meaton3@apple.com
Could you reproduce the issue, capture a sysdiagnose after the issue has been reproduced, and then upload the sysdiagnose to your bug report? Note, please add the time and date the issue was reproduced to your bug report.


Matt Eaton
DTS Engineering, CoreOS
meaton3@apple.com
@Matt: done

Follow-up: Apparently this has been fixed for single level subdomains like foo.apple.com but is still not correctly implemented for multiple levels like foo.bar.apple.com iOS 15.4.1, iPhone SE 1

It seems like this is still an issue for multilevel subdomains, is that correct?

SSL Pinning: Article "Identity Pinning: How to configure server certificates for your app"
 
 
Q