ssl 使用Swift和NSURLSession绑定iOS证书

zf9nrax1  于 12个月前  发布在  Swift
关注(0)|答案(8)|浏览(139)

如何在Swift中为NSURLSession添加证书固定?
OWASP网站只包含一个C2C和NSURLConnection的示例。

zf2sa74q

zf2sa74q1#

**Swift 3+**更新:

只需为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可以找到Gist for Swift 2 here - from the initial answer
然后使用openssl为网站创建.der文件

openssl s_client -connect my-https-website.com:443 -showcerts < /dev/null | openssl x509 -outform DER > my-https-website.der


并将其添加到xcode项目中。仔细检查它是否存在于Copy Bundle Resources列表中的Build phases选项卡中。否则将其拖放到该列表中。
最后,在代码中使用它来发出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")
}

00jrzges

00jrzges2#

多亏了这个网站上的例子:https://www.bugsee.com/blog/ssl-certificate-pinning-in-mobile-applications/,我构建了一个固定公钥而不是整个证书的版本(如果定期更新证书,会更方便)。

**更新:**删除了强制解包并替换了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();
}

}

字符串

r3i60tvu

r3i60tvu3#

这是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)
    }

}

字符串

2eafrhcq

2eafrhcq4#

保存您的网站的证书(作为.cer文件)在主包中。然后使用this URLSessionDelegate方法:

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))
}

kse8i1jr

kse8i1jr5#

提醒一下,SecTrustEvaluate已被弃用,应替换为SecTrustEvaluateWithError

所以这个:

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
   }
}

字符串
我写// Proceed with evaluation部分的原因是因为你应该验证secresult,这可能意味着证书实际上是无效的。你可以选择覆盖它,并添加任何提出的问题作为例外,最好在提示用户做出决定之后。

应该是这样的:

if SecTrustEvaluateWithError(server, nil) {
   // Certificate is valid, proceed.
}


第二个参数将捕获任何错误,但如果您对细节不感兴趣,则可以传递nil

kknvjkwl

kknvjkwl6#

@lifeisfoo的答案中的openssl命令将在OS X中为某些使用ECDSA等较新密码的SSL证书提供给予错误。
如果在@lifeisfoo的回答中运行openssl命令时出现以下错误:

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

字符串
您网站的SSL证书可能使用的算法在OS X的默认openssl版本(v0.9.X,不支持ECDSA等)中不受支持。

这里是修复:

要获得正确的.der文件,您必须首先执行brew install openssl,然后将来自@lifeisfoo的答案的openssl命令替换为:
/usr/local/Cellar/openssl/1.0.2h_1/bin/openssl [rest of the above command]
Homebrew install命令:

/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"


希望能帮上忙。

d4so4syb

d4so4syb7#

你可以试试这个

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)
}
}

字符串
来源:https://www.steveclarkapps.com/using-certificate-pinning-xcode/

nlejzf6q

nlejzf6q8#

这个问题很老了,所有的答案可能都是正确的。但是对于2021年之后遇到这个问题的人来说,现在有一个**official no-code solution described by Apple in a short blog article**。
它基于 App Transport Security 的概念,在macOS和iOS上的所有应用上强制执行加密网络连接。现在,您只需将服务器URL及其(CA)证书哈希列表添加到Info.plist中即可将其固定。如果您的端点的证书与哈希不匹配,URLSession将返回证书错误,您可以像处理任何其他网络错误一样处理该错误。
在你的Info.plist中,你可以像这样添加它(来自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>

字符串
要获取SHA256 Base 64哈希值,请下载服务器的公共证书(通常您可以使用浏览器轻松完成此操作)并在其上运行以下OpenSSL命令:

$ 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

相关问题