Howto add certificate pinning to a NSURLSession in Swift?
The OWASP website contains only an example for Objective-C and NSURLConnection.
Howto add certificate pinning to a NSURLSession in Swift?
The OWASP website contains only an example for Objective-C and NSURLConnection.
Swift 3+ Update:
Just define a delegate class for NSURLSessionDelegate
and implement the didReceiveChallenge function (this code is adapted from the objective-c OWASP example):
class NSURLSessionPinningDelegate: NSObject, URLSessionDelegate {
func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Swift.Void) {
// Adapted from OWASP https://www.owasp.org/index.php/Certificate_and_Public_Key_Pinning#iOS
if (challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust) {
if let serverTrust = challenge.protectionSpace.serverTrust {
let isServerTrusted = SecTrustEvaluateWithError(serverTrust, nil)
if(isServerTrusted) {
if let serverCertificate = SecTrustGetCertificateAtIndex(serverTrust, 0) {
let serverCertificateData = SecCertificateCopyData(serverCertificate)
let data = CFDataGetBytePtr(serverCertificateData);
let size = CFDataGetLength(serverCertificateData);
let cert1 = NSData(bytes: data, length: size)
let file_der = Bundle.main.path(forResource: "certificateFile", ofType: "der")
if let file = file_der {
if let cert2 = NSData(contentsOfFile: file) {
if cert1.isEqual(to: cert2 as Data) {
completionHandler(URLSession.AuthChallengeDisposition.useCredential, URLCredential(trust:serverTrust))
return
}
}
}
}
}
}
}
// Pinning failed
completionHandler(URLSession.AuthChallengeDisposition.cancelAuthenticationChallenge, nil)
}
}
(you can find a Gist for Swift 2 here - from the initial answer)
Then create the .der
file for your website using openssl
openssl s_client -connect my-https-website.com:443 -showcerts < /dev/null | openssl x509 -outform DER > my-https-website.der
and add it to the xcode project. Double check that it's present in the Build phases
tab, inside the Copy Bundle Resources
list. Otherwise drag and drop it inside this list.
Finally use it in your code to make URL requests:
if let url = NSURL(string: "https://my-https-website.com") {
let session = URLSession(
configuration: URLSessionConfiguration.ephemeral,
delegate: NSURLSessionPinningDelegate(),
delegateQueue: nil)
let task = session.dataTask(with: url as URL, completionHandler: { (data, response, error) -> Void in
if error != nil {
print("error: \(error!.localizedDescription): \(error!)")
} else if data != nil {
if let str = NSString(data: data!, encoding: String.Encoding.utf8.rawValue) {
print("Received data:\n\(str)")
} else {
print("Unable to convert data to text")
}
}
})
task.resume()
} else {
print("Unable to create NSURL")
}
Thanks to the example found in this site: https://www.bugsee.com/blog/ssl-certificate-pinning-in-mobile-applications/ I built a version that pins the public key and not the entire certificate (more convenient if you renew your certificate periodically).
Update: Removed the forced unwrapping and replaced SecTrustEvaluate.
import Foundation
import CommonCrypto
class SessionDelegate : NSObject, URLSessionDelegate {
private static let rsa2048Asn1Header:[UInt8] = [
0x30, 0x82, 0x01, 0x22, 0x30, 0x0d, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86,
0xf7, 0x0d, 0x01, 0x01, 0x01, 0x05, 0x00, 0x03, 0x82, 0x01, 0x0f, 0x00
];
private static let google_com_pubkey = ["4xVxzbEegwDBoyoGoJlKcwGM7hyquoFg4l+9um5oPOI="];
private static let google_com_full = ["KjLxfxajzmBH0fTH1/oujb6R5fqBiLxl0zrl2xyFT2E="];
func urlSession(_ session: URLSession,
didReceive challenge: URLAuthenticationChallenge,
completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
guard let serverTrust = challenge.protectionSpace.serverTrust else {
completionHandler(.cancelAuthenticationChallenge, nil);
return;
}
// Set SSL policies for domain name check
let policies = NSMutableArray();
policies.add(SecPolicyCreateSSL(true, (challenge.protectionSpace.host as CFString)));
SecTrustSetPolicies(serverTrust, policies);
var isServerTrusted = SecTrustEvaluateWithError(serverTrust, nil);
if(isServerTrusted && challenge.protectionSpace.host == "www.google.com") {
let certificate = SecTrustGetCertificateAtIndex(serverTrust, 0);
//Compare public key
if #available(iOS 10.0, *) {
let policy = SecPolicyCreateBasicX509();
let cfCertificates = [certificate] as CFArray;
var trust: SecTrust?
SecTrustCreateWithCertificates(cfCertificates, policy, &trust);
guard trust != nil, let pubKey = SecTrustCopyPublicKey(trust!) else {
completionHandler(.cancelAuthenticationChallenge, nil);
return;
}
var error:Unmanaged<CFError>?
if let pubKeyData = SecKeyCopyExternalRepresentation(pubKey, &error) {
var keyWithHeader = Data(bytes: SessionDelegate.rsa2048Asn1Header);
keyWithHeader.append(pubKeyData as Data);
let sha256Key = sha256(keyWithHeader);
if(!SessionDelegate.google_com_pubkey.contains(sha256Key)) {
isServerTrusted = false;
}
} else {
isServerTrusted = false;
}
} else { //Compare full certificate
let remoteCertificateData = SecCertificateCopyData(certificate!) as Data;
let sha256Data = sha256(remoteCertificateData);
if(!SessionDelegate.google_com_full.contains(sha256Data)) {
isServerTrusted = false;
}
}
}
if(isServerTrusted) {
let credential = URLCredential(trust: serverTrust);
completionHandler(.useCredential, credential);
} else {
completionHandler(.cancelAuthenticationChallenge, nil);
}
}
func sha256(_ data : Data) -> String {
var hash = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH))
data.withUnsafeBytes {
_ = CC_SHA256($0, CC_LONG(data.count), &hash)
}
return Data(bytes: hash).base64EncodedString();
}
}
rsa2048Asn1Header
was really helpful. –
Inattentive Here's an updated version for Swift 3
import Foundation
import Security
class NSURLSessionPinningDelegate: NSObject, URLSessionDelegate {
func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Swift.Void) {
// Adapted from OWASP https://www.owasp.org/index.php/Certificate_and_Public_Key_Pinning#iOS
if (challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust) {
if let serverTrust = challenge.protectionSpace.serverTrust {
var secresult = SecTrustResultType.invalid
let status = SecTrustEvaluate(serverTrust, &secresult)
if(errSecSuccess == status) {
if let serverCertificate = SecTrustGetCertificateAtIndex(serverTrust, 0) {
let serverCertificateData = SecCertificateCopyData(serverCertificate)
let data = CFDataGetBytePtr(serverCertificateData);
let size = CFDataGetLength(serverCertificateData);
let cert1 = NSData(bytes: data, length: size)
let file_der = Bundle.main.path(forResource: "name-of-cert-file", ofType: "cer")
if let file = file_der {
if let cert2 = NSData(contentsOfFile: file) {
if cert1.isEqual(to: cert2 as Data) {
completionHandler(URLSession.AuthChallengeDisposition.useCredential, URLCredential(trust:serverTrust))
return
}
}
}
}
}
}
}
// Pinning failed
completionHandler(URLSession.AuthChallengeDisposition.cancelAuthenticationChallenge, nil)
}
}
Save the certificate (as .cer file) of your website in the main bundle. Then use this URLSessionDelegate method:
func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
guard
challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust,
let serverTrust = challenge.protectionSpace.serverTrust,
SecTrustEvaluate(serverTrust, nil) == errSecSuccess,
let serverCert = SecTrustGetCertificateAtIndex(serverTrust, 0) else {
reject(with: completionHandler)
return
}
let serverCertData = SecCertificateCopyData(serverCert) as Data
guard
let localCertPath = Bundle.main.path(forResource: "shop.rewe.de", ofType: "cer"),
let localCertData = NSData(contentsOfFile: localCertPath) as Data?,
localCertData == serverCertData else {
reject(with: completionHandler)
return
}
accept(with: serverTrust, completionHandler)
}
...
func reject(with completionHandler: ((URLSession.AuthChallengeDisposition, URLCredential?) -> Void)) {
completionHandler(.cancelAuthenticationChallenge, nil)
}
func accept(with serverTrust: SecTrust, _ completionHandler: ((URLSession.AuthChallengeDisposition, URLCredential?) -> Void)) {
completionHandler(.useCredential, URLCredential(trust: serverTrust))
}
Just a heads up, SecTrustEvaluate
is deprecated and should be replaced with SecTrustEvaluateWithError
.
So this:
var secresult = SecTrustResultType.invalid
let status = SecTrustEvaluate(serverTrust, &secresult)
if errSecSuccess == status {
// Proceed with evaluation
switch result {
case .unspecified, .proceed: return true
default: return false
}
}
The reason i wrote the // Proceed with evaluation
section is because you should validate the secresult
as well as this could imply that the certificate is actually invalid. You have the option to override this and add any raised issues as exceptions, preferably after prompting the user for a decision.
Should be this:
if SecTrustEvaluateWithError(server, nil) {
// Certificate is valid, proceed.
}
The second param will capture any error, but if you are not interested in the specifics, you can just pass nil
.
This question is quite old, and all the answers are probably still correct. But for everybody stumbling on this after 2021, there is now an official no-code solution described by Apple in a short blog article.
It is based on the App Transport Security concept that enforces encrypted network connection on all apps on macOS and iOS. You can now simply add a list of server URLs and their (CA) certificate hashes to your Info.plist
to pin them.
If the certificate of your endpoint does not match the hash, URLSession
will return a certificate error that you can handle like any other network error.
In your Info.plist
you add it like this (sample code from Apple):
<key>NSAppTransportSecurity</key>
<dict>
<key>NSPinnedDomains</key>
<dict>
<key>example.org</key>
<dict>
<key>NSIncludesSubdomains</key>
<true/>
<key>NSPinnedCAIdentities</key>
<array>
<dict>
<key>SPKI-SHA256-BASE64</key>
<string>r/mIkG3eEpVdm+u/ko/cwxzOMo1bk4TyHIlByibiA5E=</string>
</dict>
</array>
</dict>
</dict>
</dict>
To get the SHA256 Base 64 hash value, download the server's public certificate (usually you can easily do that with your browser) and run the following OpenSSL commands on it:
$ cat ca.pem | openssl x509 -inform pem -noout -outform pem -pubkey | openssl pkey -pubin -inform pem -outform der | openssl dgst -sha256 -binary | openssl enc -base64
The openssl
command in @lifeisfoo's answer will give an error in OS X for certain SSL certificates that use newer ciphers like ECDSA.
If you're getting the following error when you run the openssl
command in @lifeisfoo's answer:
write:errno=54
unable to load certificate
1769:error:0906D06C:PEM routines:PEM_read_bio:no start
line:/BuildRoot/Library/Caches/com.apple.xbs/Sources/OpenSSL098/OpenSSL09
8-59.60.1/src/crypto/pem/pem_lib.c:648:Expecting: TRUSTED CERTIFICATE
You're website's SSL certificate probably is using an algorithm that isn't supported in OS X's default openssl
version (v0.9.X, which does NOT support ECDSA, among others).
Here's the fix:
To get the proper .der
file, you'll have to first brew install openssl
, and then replace the openssl
command from @lifeisfoo's answer with:
/usr/local/Cellar/openssl/1.0.2h_1/bin/openssl [rest of the above command]
Homebrew install command:
/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
hope that helps.
configuration: NSURLSessionConfiguration.ephemeralSessionConfiguration()
, to configuration: NSURLSessionConfiguration.defaultSessionConfiguration()
–
Sassy You can try this.
import Foundation
import Security
class NSURLSessionPinningDelegate: NSObject, URLSessionDelegate {
let certFileName = "name-of-cert-file"
let certFileType = "cer"
func urlSession(_ session: URLSession,
didReceive challenge: URLAuthenticationChallenge,
completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Swift.Void) {
if (challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust) {
if let serverTrust = challenge.protectionSpace.serverTrust {
var secresult = SecTrustResultType.invalid
let status = SecTrustEvaluate(serverTrust, &secresult)
if(errSecSuccess == status) {
if let serverCertificate = SecTrustGetCertificateAtIndex(serverTrust, 0) {
let serverCertificateData = SecCertificateCopyData(serverCertificate)
let data = CFDataGetBytePtr(serverCertificateData);
let size = CFDataGetLength(serverCertificateData);
let certificateOne = NSData(bytes: data, length: size)
let filePath = Bundle.main.path(forResource: self.certFileName,
ofType: self.certFileType)
if let file = filePath {
if let certificateTwo = NSData(contentsOfFile: file) {
if certificateOne.isEqual(to: certificateTwo as Data) {
completionHandler(URLSession.AuthChallengeDisposition.useCredential,
URLCredential(trust:serverTrust))
return
}
}
}
}
}
}
}
// Pinning failed
completionHandler(URLSession.AuthChallengeDisposition.cancelAuthenticationChallenge, nil)
}
}
Source: https://www.steveclarkapps.com/using-certificate-pinning-xcode/
© 2022 - 2024 — McMap. All rights reserved.
repeat {...} while(false)
has not purpose here. In the OWASP Obj-C code it functions tobreak
out of the code in case of failure but as you structured theif
statements differently it has no purpose anymore. – Blanchblancha