Swift自签名证书SSL错误

22

这段代码尝试访问一个在浏览器中可用的SSL URL,但失败了:

let path = "https://localhost:8443/greeting"
let request = NSMutableURLRequest(URL: NSURL(string: path)!)
let session = NSURLSession.sharedSession()

let task = session.dataTaskWithRequest(request, completionHandler: {data, response, error -> Void in
    let json:JSON = JSON(data: data!)
    if let c = json["content"].string {
        print(c)
    }
})
task.resume()

出现以下错误:

Optional(Error Domain=NSURLErrorDomain Code=-1200 "发生 SSL 错误,无法建立到服务器的安全连接。" UserInfo={NSURLErrorFailingURLPeerTrustErrorKey=,

需要做什么才能让应用程序接受此证书?

所涉及的证书是自签名的。 在 SO 上读了几个解决方案但没有成功。

运行 Xcode 7.2。


你信任这个证书吗?就像这个问题中所问的:https://dev59.com/2GIk5IYBdhLWcg3wL7fj - ljk321
4
这是 ATS 的问题吗?请检查您的属性列表(plist)文件。https://dev59.com/YVwY5IYBdhLWcg3wU2bA#32955793 - Ashish Kakkad
@skyline75489 - 我尝试了 - 已更新问题。 - Marcus Leon
谢谢Ashish,那就是关键! - Marcus Leon
4个回答

24

@Ashish Kakkad说得对。这个可以运行:

class Blah: NSURLSessionDelegate {

    func rest() {
        let path = "https://localhost:8443/greeting"
        let request = NSMutableURLRequest(URL: NSURL(string: path)!)
        let configuration = NSURLSessionConfiguration.defaultSessionConfiguration()
        let session = NSURLSession(configuration: configuration, delegate: self, delegateQueue:NSOperationQueue.mainQueue())
        let task = session.dataTaskWithRequest(request, completionHandler: {data, response, error -> Void in
            let json:JSON = JSON(data: data!)
            if let c = json["content"].string {
                print(c)
            }
        })
        task.resume()
    }

    func URLSession(session: NSURLSession, task: NSURLSessionTask, didReceiveChallenge challenge: NSURLAuthenticationChallenge, completionHandler: (NSURLSessionAuthChallengeDisposition, NSURLCredential?) -> Void) {
        completionHandler(NSURLSessionAuthChallengeDisposition.UseCredential, NSURLCredential(forTrust: challenge.protectionSpace.serverTrust!))
    }
}

在 Info.plist 文件中加入以下内容:

<key>NSAppTransportSecurity</key>
<dict>
    <key>NSExceptionDomains</key>
    <dict>
        <key>localhost</key>
        <dict>
            <key>NSExceptionAllowsInsecureHTTPLoads</key>
            <true/>
        </dict>
    </dict>
</dict>

2
这个方法可以运行,但只是一个权宜之计。另外请注意,使用ATS创建的异常很快将被弃用。 - midori
我还从其他开发人员那里得到了消息,苹果不允许在应用商店中使用此设置的应用程序。 - Relsell
2
这个解决方案完全忽略了证书,而不是尝试确认用户是否使用自签名证书。 - drewster
这不是一个解决方案。通过这种方式,您无法防止中间人攻击。您正在批准所有证书,而没有检查证书是否有效。 - Karlicic Bojan

17

Sai Reddy 的解决方案允许您接受自签名证书,如果它具有完整的链路,则也接受其他证书。

Marcus Leon 的解决方案是完全覆盖 - 基本上忽略所有证书。

我更喜欢这个。

Swift 4.1,iOS 11.4.1

首先,在您的 Info.plist 文件中:

<key>NSAppTransportSecurity</key>
    <dict>
        <key>NSAllowsArbitraryLoads</key>
        <true/>
    </dict>

其次,无论在何处使用您的NSURLSession,都请使用类似以下代码而不是设置为URLSession.shared:

session = URLSession(configuration: .default, delegate: APIURLSessionTaskDelegate(isSSLPinningEnabled: isSSLPinningEnabled), delegateQueue: nil)

然后添加这个类来处理固定:
    func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {

        print("*** received SESSION challenge...\(challenge)")
        let trust = challenge.protectionSpace.serverTrust!
        let credential = URLCredential(trust: trust)

        guard isSSLPinningEnabled else {
            print("*** SSL Pinning Disabled -- Using default handling.")
            completionHandler(.useCredential, credential)
            return
        }

        let myCertName = "my_certificate_to_pin"
        var remoteCertMatchesPinnedCert = false
        if let myCertPath = Bundle.main.path(forResource: myCertName, ofType: "der") {
            if let pinnedCertData = NSData(contentsOfFile: myCertPath) {
                // Compare certificate data
                let remoteCertData: NSData = SecCertificateCopyData(SecTrustGetCertificateAtIndex(trust, 0)!)
                if remoteCertData.isEqual(to: pinnedCertData as Data) {
                    print("*** CERTIFICATE DATA MATCHES")
                    remoteCertMatchesPinnedCert = true
                }
                else {
                    print("*** MISMATCH IN CERT DATA.... :(")
                }

            } else {
                print("*** Couldn't read pinning certificate data")
            }
        } else {
            print("*** Couldn't load pinning certificate!")
        }

        if remoteCertMatchesPinnedCert {
            print("*** TRUSTING CERTIFICATE")
            completionHandler(.useCredential, credential)
        } else {
            print("NOT TRUSTING CERTIFICATE")
            completionHandler(.rejectProtectionSpace, nil)
        }
    }
}

这个类检查您是否启用了证书固定。如果是,则完全忽略正常的证书验证,并使用我们在应用程序中包含的证书进行精确比较。通过这种方式,它接受您自签名的证书,而不接受其他任何证书。
此解决方案要求您在项目中的资源文件夹中放置一个名为“my_certificate_to_pin.der”的文件。如果您还没有资源文件夹,请添加一个。
该证书应以DER格式提供。
要为您的服务器创建自签名证书,通常会执行以下操作:
openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout mycert.key -out mycert.cer

这将生成两个文件 -- 一个mycert.key私钥文件和一个mycert.cer证书本身。它们都是X509格式的。对于iOS,您需要以DER格式获取证书,因此请执行以下操作:

openssl x509 -outform der -in mycert.cer -out my_certificate_to_pin.der

这将在iOS上生成您所需的文件。


如果我想在模拟器中允许所有证书怎么办?编译代码时有什么可以添加的吗? - Aaron Bratcher
只需在前两个let语句(“let trust ...”和“let credential ...”)之后添加“completionHandler(.useCredential,credential)”,它就会接受所有内容。 - drewster
通常这是因为证书不是DER格式。请参见上面的openssl命令。 - drewster
这个第一次就成功了。为什么它不是被接受的答案呢? - Nick

3

您可以使用自己的证书而不是我的证书(fullchain.pem)。

    class AccessingServer: NSObject,URLSessionDelegate {

    func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {

        if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust {
            // First load our extra root-CAs to be trusted from the app bundle.
            let trust = challenge.protectionSpace.serverTrust

            let rootCa = "fullchain"
            if let rootCaPath = Bundle.main.path(forResource: rootCa, ofType: "pem") {
                if let rootCaData = NSData(contentsOfFile: rootCaPath) {

                    let rootCert = SecCertificateCreateWithData(nil, rootCaData)!

                    SecTrustSetAnchorCertificates(trust!, [rootCert] as CFArray)

                    SecTrustSetAnchorCertificatesOnly(trust!, false)
                }
            }

            var trustResult: SecTrustResultType = SecTrustResultType.invalid
            SecTrustEvaluate(trust!, &trustResult)

            if (trustResult == SecTrustResultType.unspecified ||
                trustResult == SecTrustResultType.proceed) {
                // Trust certificate.

                let credential = URLCredential(trust: challenge.protectionSpace.serverTrust!)
                challenge.sender?.use(credential, for: challenge)

            } else {
                NSLog("Invalid server certificate.")
                challenge.sender?.cancel(challenge)
            }
        } else {
            NSLog("Got unexpected authentication method \(challenge.protectionSpace.authenticationMethod)");
            challenge.sender?.cancel(challenge)
        }
    }

   }

从哪里获取证书? - Rajesh Maurya
#Rajesh证书由后台团队提供。 - Sai kumar Reddy
1
我从钥匙串中导出了证书。在这一行代码 SecTrustSetAnchorCertificates(trust!, rootCert as! CFArray) 中,程序崩溃了。 - Rajesh Maurya
为了避免从SecTrustSetAnchorCertificates获取null:https://dev59.com/questions/Rpvga4cB1Zd3GeqP7c2g - xarly

1

Swift 5.1

我知道你已经找到了答案,但如果有人能使用它,我有一个更短的方法。

class AuthViewModel: NSObject, ObservableObject {

func login(username: String, password: String,completion: @escaping ((LoginModel) -> Void)) -> Void {
    
    let url = URL(string: "https://YourAPI.xyz/APP/?action=login&user=\(username)&pass=\(password)")!

    // Here is Important ****
    let req = URLSession(configuration: URLSessionConfiguration.default, delegate: self, delegateQueue: nil)
    // *****

    let task = req.dataTask(with: request) { data1, response, error in
        guard
            let data = data1,
            let response = response as? HTTPURLResponse,
            error == nil
        else {                                                               // check for fundamental networking error
            print("error", error ?? URLError(.badServerResponse))
            return
        }
        
        guard (200 ... 299) ~= response.statusCode else {                    // check for http errors
            print("statusCode should be 2xx, but is \(response.statusCode)")
            print("response = \(response)")
            return
        }
        
        // do whatever you want with the `data`, e.g.:
        
        
        DispatchQueue.main.async {
            do {
                let resStreing = String(data: data, encoding: .utf8)
                let resData = resStreing?.data(using: .utf8)
                
                let responseObject = try JSONDecoder().decode(LoginModel.self, from: resData!)
                
                print("\n\n ResponseObject = \(responseObject) \n\n")
                completion(responseObject)
            } catch {
                print("CAN NOT PARSE!") // parsing error
                
                if let responseString = String(data: data, encoding: .utf8) {
                    print("responseString = \(responseString)")
                } else {
                    print("unable to parse response as string")
                }
            }
        }
    }

    task.resume()
  }
}

您需要的扩展名为:
extension AuthViewModel: URLSessionDelegate {
public func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
   //Trust the certificate even if not valid
   let urlCredential = URLCredential(trust: challenge.protectionSpace.serverTrust!)

   completionHandler(.useCredential, urlCredential)
}
}

使用方法:

authviewModel.login(username: "UserName",password: "Password") { loginModel in
// Do something
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接