使用 Swift 和 NSURLSession 固定 iOS 证书
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/34223291/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
iOS certificate pinning with Swift and NSURLSession
提问by lifeisfoo
Howto add certificate pinning to a NSURLSession in Swift?
如何在 Swift 中向 NSURLSession 添加证书固定?
The OWASP websitecontains only an example for Objective-C and NSURLConnection.
该OWASP网站只包含Objective-C和NSURLConnection的一个例子。
回答by lifeisfoo
Swift 3+Update:
斯威夫特 3+更新:
Just define a delegate class for NSURLSessionDelegate
and implement the didReceiveChallenge function (this code is adapted from the objective-c OWASP example):
只需定义一个委托类NSURLSessionDelegate
并实现 didReceiveChallenge 函数(此代码改编自objective-c OWASP示例):
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)
(您可以在此处找到Swift 2的要点 - 从最初的答案开始)
Then create the .der
file for your website using openssl
然后.der
使用为您的网站创建文件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.
并将其添加到 xcode 项目中。仔细检查它是否存在于列表中的Build phases
选项卡Copy Bundle Resources
中。否则将其拖放到此列表中。
Finally use it in your code to make URL requests:
最后在您的代码中使用它来发出 URL 请求:
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")
}
回答by Larcho
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).
感谢在这个站点中找到的示例:https: //www.bugsee.com/blog/ssl-certificate-pinning-in-mobile-applications/我构建了一个固定公钥而不是整个证书的版本(更方便如果您定期更新证书)。
Update:Removed the forced unwrapping and replaced SecTrustEvaluate.
更新:删除了强制解包并替换了 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(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)
}
}
, CC_LONG(data.count), &hash)
}
return Data(bytes: hash).base64EncodedString();
}
}
回答by Trevis Thomas
Here's an updated version for Swift 3
这是 Swift 3 的更新版本
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)
}
回答by schirrmacher
Save the certificate (as .cer file) of your website in the main bundle. Then use thisURLSessionDelegate method:
将您网站的证书(作为 .cer 文件)保存在主包中。然后使用这个URLSessionDelegate 方法:
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))
}
...
...
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
}
}
回答by thecivillain
Just a heads up, SecTrustEvaluate
is deprecated and should be replaced with SecTrustEvaluateWithError
.
只是提醒一下,SecTrustEvaluate
已弃用,应替换为SecTrustEvaluateWithError
.
So this:
所以这:
if SecTrustEvaluateWithError(server, nil) {
// Certificate is valid, proceed.
}
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.
我写这个// Proceed with evaluation
部分的原因是因为你应该验证secresult
以及这可能意味着证书实际上是无效的。您可以选择覆盖此选项并将任何引发的问题添加为例外,最好是在提示用户做出决定之后。
Should be this:
应该是这样的:
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
The second param will capture any error, but if you are not interested in the specifics, you can just pass nil
.
第二个参数将捕获任何错误,但如果您对细节不感兴趣,您可以直接通过nil
.
回答by weltan
The openssl
command in @lifeisfoo's answer will give an error in OS X for certain SSL certificates that use newer ciphers like ECDSA.
openssl
@lifeisfoo's answer 中的命令将在 OS X 中为某些使用较新密码(如 ECDSA)的 SSL 证书给出错误。
If you're getting the following error when you run the openssl
command in @lifeisfoo's answer:
如果openssl
在@lifeisfoo 的回答中运行命令时出现以下错误:
/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
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).
您网站的 SSL 证书可能正在使用 OS X 的默认openssl
版本(v0.9.X,不支持 ECDSA 等)不支持的算法。
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:
要获得正确的.der
文件,您必须先将@lifeisfoo 的答案中brew install openssl
的openssl
命令替换为:
/usr/local/Cellar/openssl/1.0.2h_1/bin/openssl [rest of the above command]
/usr/local/Cellar/openssl/1.0.2h_1/bin/openssl [rest of the above command]
Homebrew install command:
Homebrew 安装命令:
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)
}
}
hope that helps.
希望有帮助。
回答by Sour LeangChhean
You can try this.
你可以试试这个。
##代码##Source: https://www.steveclarkapps.com/using-certificate-pinning-xcode/
来源:https: //www.steveclarkapps.com/using-certificate-pinning-xcode/