如何在iOS上固定证书的公钥
在改进我们正在开发的iOS应用程序的安全性的同时,我们发现需要对服务器的SSL证书进行PIN(全部或部分)以防止中间人(man-in-the-middle)攻击。
即使有不同的方法来做到这一点,当你search这个我只find钉住整个证书的例子。 这种做法会带来一个问题:一旦证书更新,您的应用程序将无法再连接。 如果您select固定公钥而不是整个证书,您将会发现自己(我相信)处于同样安全的状况,同时在服务器中的证书更新更有弹性。
但是,你如何做到这一点?
如果你需要知道如何从你的iOS代码中的证书中提取这些信息,在这里你有一个办法。
首先添加安全框架。
#import <Security/Security.h>
添加openssl库。 你可以从https://github.com/st3fan/ios-openssl下载它们;
#import <openssl/x509.h>
NSURLConnectionDelegate协议允许您决定连接是否应该能够响应保护空间。 简而言之,就是您可以查看来自服务器的证书,并决定是允许连接继续还是取消。 你想在这里做的是比较证书的公钥和你固定的那个。 现在的问题是,你如何获得这样的公钥? 看看下面的代码:
首先获取X509格式的证书(您需要使用ssl库)
const unsigned char *certificateDataBytes = (const unsigned char *)[serverCertificateData bytes]; X509 *certificateX509 = d2i_X509(NULL, &certificateDataBytes, [serverCertificateData length]);
现在我们准备读取公钥数据
ASN1_BIT_STRING *pubKey2 = X509_get0_pubkey_bitstr(certificateX509); NSString *publicKeyString = [[NSString alloc] init];
此时,您可以遍历pubKey2string,并使用以下循环将hex格式的字节提取为string
for (int i = 0; i < pubKey2->length; i++) { NSString *aString = [NSString stringWithFormat:@"%02x", pubKey2->data[i]]; publicKeyString = [publicKeyString stringByAppendingString:aString]; }
打印公共密钥以查看它
NSLog(@"%@", publicKeyString);
完整的代码
- (BOOL)connection:(NSURLConnection *)connection canAuthenticateAgainstProtectionSpace:(NSURLProtectionSpace *)protectionSpace { const unsigned char *certificateDataBytes = (const unsigned char *)[serverCertificateData bytes]; X509 *certificateX509 = d2i_X509(NULL, &certificateDataBytes, [serverCertificateData length]); ASN1_BIT_STRING *pubKey2 = X509_get0_pubkey_bitstr(certificateX509); NSString *publicKeyString = [[NSString alloc] init]; for (int i = 0; i < pubKey2->length; i++) { NSString *aString = [NSString stringWithFormat:@"%02x", pubKey2->data[i]]; publicKeyString = [publicKeyString stringByAppendingString:aString]; } if ([publicKeyString isEqual:myPinnedPublicKeyString]){ NSLog(@"YES THEY ARE EQUAL, PROCEED"); return YES; }else{ NSLog(@"Security Breach"); [connection cancel]; return NO; } }
据我可以告诉你不能直接在iOS中创build预期的公钥,你需要通过证书来做到这一点。 所以所需的步骤与固定证书类似,但是还需要从实际证书和参考证书(预期的公钥)中提取公钥。
你需要做的是:
- 使用NSURLConnectionDelegate检索数据,并实现
willSendRequestForAuthenticationChallenge
。 - 以DER格式包含参考证书。 在这个例子中,我使用了一个简单的资源文件。
- 提取服务器提供的公钥
- 从您的参考证书中提取公钥
- 比较两者
- 如果它们匹配,继续进行常规检查(主机名,证书签名等)
- 如果不匹配,则失败。
一些示例代码:
(void)connection:(NSURLConnection *)connection willSendRequestForAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge { // get the public key offered by the server SecTrustRef serverTrust = challenge.protectionSpace.serverTrust; SecKeyRef actualKey = SecTrustCopyPublicKey(serverTrust); // load the reference certificate NSString *certFile = [[NSBundle mainBundle] pathForResource:@"ref-cert" ofType:@"der"]; NSData* certData = [NSData dataWithContentsOfFile:certFile]; SecCertificateRef expectedCertificate = SecCertificateCreateWithData(NULL, (__bridge CFDataRef)certData); // extract the expected public key SecKeyRef expectedKey = NULL; SecCertificateRef certRefs[1] = { expectedCertificate }; CFArrayRef certArray = CFArrayCreate(kCFAllocatorDefault, (void *) certRefs, 1, NULL); SecPolicyRef policy = SecPolicyCreateBasicX509(); SecTrustRef expTrust = NULL; OSStatus status = SecTrustCreateWithCertificates(certArray, policy, &expTrust); if (status == errSecSuccess) { expectedKey = SecTrustCopyPublicKey(expTrust); } CFRelease(expTrust); CFRelease(policy); CFRelease(certArray); // check a match if (actualKey != NULL && expectedKey != NULL && [(__bridge id) actualKey isEqual:(__bridge id)expectedKey]) { // public keys match, continue with other checks [challenge.sender performDefaultHandlingForAuthenticationChallenge:challenge]; } else { // public keys do not match [challenge.sender cancelAuthenticationChallenge:challenge]; } if(actualKey) { CFRelease(actualKey); } if(expectedKey) { CFRelease(expectedKey); } }
免责声明:这只是示例代码,没有经过彻底的testing。 对于完整的实现,从OWASP的证书locking示例开始。
请记住,使用SSL Kill Switch和类似的工具总是可以避免证书locking。
您可以使用Security.framework的SecTrustCopyPublicKey
函数执行公钥SSL固定。 请参阅AFNetworking项目的连接示例:willSendRequestForAuthenticationChallenge :。
如果你需要iOS版的openSSL,请使用https://gist.github.com/foozmeat/5154962它基于st3fan / ios-openssl,目前无法运行。
你可以使用这里提到的PhoneGap(Build)插件: http : //www.x-services.nl/certificate-pinning-plugin-for-phonegap-to-prevent-man-in-the-middle-attacks/734
该插件支持多个证书,所以服务器和客户端不需要同时更新。 如果您的指纹每两年(例如)改变一次,那么实现一个强制客户端更新的机制(向您的应用程序添加一个版本,并在服务器上创build一个'minimalRequiredVersion'API方法。太低(当新的证书被激活时,fi)。
如果您使用AFNetworking(更具体地说,AFSecurityPolicy),并且select模式AFSSLPinningModePublicKey,则只要公钥保持不变,证书是否更改并不重要。 是的,AFSecurityPolicy确实没有提供直接设置公钥的方法; 您只能通过调用setPinnedCertificates
设置您的证书。 但是,如果查看setPinnedCertificates的实现,则会看到框架正在从证书中提取公用密钥,然后比较密钥。
总之,通过证书,不要担心他们在未来的变化。 框架只关心这些证书中的公钥。
以下代码适用于我。
AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager]; manager.securityPolicy = [AFSecurityPolicy policyWithPinningMode:AFSSLPinningModePublicKey]; [manager.securityPolicy setPinnedCertificates:myCertificate];
…固定整个证书。 这种做法是一个问题
此外,Google每月(或如此)更改证书,但保留或重新authentication公众。 因此,证书locking会导致大量的虚假警告,而公钥locking将通过关键的连续性testing。
我相信Google会保持CRL,OCSP和撤销列表的可pipe理性,我希望其他人也可以这样做。 对于我的网站,我通常会重新authentication密钥,以确保密钥的连续性。
但是,你如何做到这一点?
证书和公钥密码 。 本文将讨论这个练习,并提供OpenSSL,Android,iOS和.Net的示例代码。 iOS上讨论的框架至less存在一个问题:从NSUrlConnection didReceiveAuthenticationChallenge(证书失败)中提供有意义的错误 。
此外,彼得·古特曼(Peter Gutmann)在他的着作“ 工程安全 ”( Engineering Security)中对关键连续性和固定作了很好的处理
如果您使用AFNetworking,请使用AFSecurityPolicy *policy = [AFSecurityPolicy policyWithPinningMode:AFSSLPinningModePublicKey];
这里的Swifty答案。 保存您的网站在主包中的证书(作为.cer文件)。 然后使用这个 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)) }
你可以像这样使用Chrome来获取.cer文件。