如何在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预期的公钥,你需要通过证书来做到这一点。 所以所需的步骤与固定证书类似,但是还需要从实际证书和参考证书(预期的公钥)中提取公钥。

你需要做的是:

  1. 使用NSURLConnectionDelegate检索数据,并实现willSendRequestForAuthenticationChallenge
  2. 以DER格式包含参考证书。 在这个例子中,我使用了一个简单的资源文件。
  3. 提取服务器提供的公钥
  4. 从您的参考证书中提取公钥
  5. 比较两者
  6. 如果它们匹配,继续进行常规检查(主机名,证书签名等)
  7. 如果不匹配,则失败。

一些示例代码:

  (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文件。