如何在 iOS 上固定证书的公钥

Posted

技术标签:

【中文标题】如何在 iOS 上固定证书的公钥【英文标题】:How to pin the Public key of a certificate on iOS 【发布时间】:2013-03-21 15:40:51 【问题描述】:

在提高我们正在开发的 ios 应用程序的安全性时,我们发现需要对服务器的 SSL 证书进行 PIN(全部或部分)以防止中间人攻击。

尽管有多种方法可以做到这一点,但当您搜索这个时,我只找到了固定整个证书的示例。这种做法会带来一个问题:一旦证书更新,您的应用程序将无法再连接。 如果您选择固定公钥而不是整个证书,您会发现自己(我相信)处于同样安全的情况下,同时对服务器中的证书更新更有弹性。

但是你是怎么做到的呢?

【问题讨论】:

看看这个如果它可以帮助jayprakashdubey.blogspot.in/2017/07/… TrustKit github 上有一个很好的例子:github.com/datatheorem/TrustKit 【参考方案1】:

如果您需要知道如何从 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];    

此时您可以遍历 pubKey2 字符串,并将 HEX 格式的字节提取到一个字符串中,使用以下循环

 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;



【讨论】:

我们从哪里得到serverCertificateData【参考方案2】:

据我所知,您无法直接在 iOS 中轻松创建预期的公钥,您需要通过证书来完成。 因此所需的步骤类似于固定证书,但另外您需要从实际证书和参考证书(预期的公钥)中提取公钥。

你需要做的是:

    使用 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);
    
 

免责声明:这只是示例代码,未经彻底测试。 要完整实施,请从 certificate pinning example by OWASP 开始。

请记住,使用SSL Kill Switch 和类似工具始终可以避免证书锁定。

【讨论】:

干得好!允许在没有所有 openSSL 问题的情况下进行公钥比较 有人在 Swift 中有这个吗?【参考方案3】:

您可以使用 Security.framework 的 SecTrustCopyPublicKey 函数进行公钥 SSL 固定。请参阅 AFNetworking 项目的connection:willSendRequestForAuthenticationChallenge: 示例。

如果你需要 iOS 的 openSSL,请使用https://gist.github.com/foozmeat/5154962 它基于 st3fan/ios-openssl,目前无法使用。

【讨论】:

更具体地说,证书和公钥固定的实现在github.com/AFNetworking/AFNetworking/blob/master/AFNetworking/…【参考方案4】:

您可以使用此处提到的 PhoneGap (Build) 插件:http://www.x-services.nl/certificate-pinning-plugin-for-phonegap-to-prevent-man-in-the-middle-attacks/734

该插件支持多个证书,因此服务器和客户端不需要同时更新。如果您的指纹每(例如)2 年更改一次,则实施一种强制客户端更新的机制(向您的应用程序添加一个版本并在服务器上创建一个“minimalRequiredVersion”API 方法。如果应用程序版本是,请告诉客户端更新太低(激活新证书时的 fi)。

【讨论】:

【参考方案5】:

如果您使用 AFNetworking(更具体地说,AFSecurityPolicy),并且选择了 AFSSLPinningModePublicKey 模式,那么您的证书是否更改并不重要,只要公钥保持不变即可。是的,AFSecurityPolicy 确实没有为您提供直接设置公钥的方法;您只能通过调用setPinnedCertificates 来设置您的证书。但是,如果您查看 setPinnedCertificates 的实现,您会看到该框架正在从证书中提取公钥,然后比较这些密钥。

简而言之,把证书传进去,以后不用担心它们会变。该框架只关心这些证书中的公钥。

以下代码对我有用。

AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager];
manager.securityPolicy = [AFSecurityPolicy policyWithPinningMode:AFSSLPinningModePublicKey];
[manager.securityPolicy setPinnedCertificates:myCertificate];

【讨论】:

【参考方案6】:

...用于固定整个证书。这样的做法是有问题的……

此外,Google 会每月(左右)更改证书,但会保留或重新认证公众。所以证书固定会导致很多虚假警告,而公钥固定会通过密钥连续性测试。

我相信 Google 这样做是为了使 CRL、OCSP 和撤销列表易于管理,我希望其他人也会这样做。对于我的网站,我通常会重新认证密钥,以便人们确保密钥的连续性。

但是你是怎么做到的呢?

Certificate and Public Key Pinning。本文讨论了这种做法,并提供了适用于 OpenSSL、android、iOS 和 .Net 的示例代码。在iOS: Provide Meaningful Error from NSUrlConnection didReceiveAuthenticationChallenge (Certificate Failure) 讨论的框架中,iOS 存在至少一个问题。

此外,Peter Gutmann 在他的书Engineering Security 中对关键连续性和固定进行了很好的处理。

【讨论】:

OWASP 中的 iOS 项目示例并没有真正做公钥固定,而是证书固定。如果您碰巧进入他们的代码,您会看到他们所做的比较是:const BOOL equal = [cert1 isEqualToData:cert2]; 他们基本上是在比较整个证书!好吧,要么那个,要么我错过了什么...... 是的,比较证书有一个问题,如果您续订证书,则必须使用新证书更新您的应用程序。与比较固定的公钥相反,您可以在其中更新证书但保持公钥相同,因此无需更新应用程序。【参考方案7】:

如果您使用 AFNetworking,请使用 AFSecurityPolicy *policy = [AFSecurityPolicy policyWithPinningMode:AFSSLPinningModePublicKey];

【讨论】:

以上是关于如何在 iOS 上固定证书的公钥的主要内容,如果未能解决你的问题,请参考以下文章

如何为公钥固定(SSL 固定)生成 iOS 证书

证书固定 - 仅公钥?

如何使用证书中的公钥检查 ECDSA(在 p-256 上)签名

SSL pinning - 在 AFSecurityPolicy 中设置固定公钥而不是固定证书

如何允许用户在 iOS 中使用 AFNetworking 信任和固定自签名 SSL 证书

如何在生产环境中替换 iOS 证书? [复制]