iOS certificate pinning with Swift and NSURLSession

前端 未结 7 821
无人及你
无人及你 2020-12-07 18:04

Howto add certificate pinning to a NSURLSession in Swift?

The OWASP website contains only an example for Objective-C and NSURLConnection.

相关标签:
7条回答
  • 2020-12-07 18:33

    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))
    }
    
    0 讨论(0)
  • 2020-12-07 18:37

    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();
    }
    
    }
    
    0 讨论(0)
  • 2020-12-07 18:41

    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.

    0 讨论(0)
  • 2020-12-07 18:43

    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)
        }
    
    }
    
    0 讨论(0)
  • 2020-12-07 18:43

    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?) -&gt; 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/

    0 讨论(0)
  • 2020-12-07 18:46

    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.

    0 讨论(0)
提交回复
热议问题