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

52

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

尽管有各种方法可以实现此目的,但当您搜索此时,我们只找到了一些针对整个证书进行PIN码的示例。这种做法存在一个问题:一旦证书更新,您的应用程序将无法再连接。如果您选择固定公钥而不是整个证书,则会发现自己处于同样安全的情况下,同时更能抵御服务器证书更新所带来的影响。

但是如何做到这一点呢?


如果需要帮助,请查看此链接:http://jayprakashdubey.blogspot.in/2017/07/ssl-pinning-in-ios-swift-code.html - Jayprakash Dubey
在TrustKit的github上有一个很好的例子:https://github.com/datatheorem/TrustKit - grebulon
7个回答

36

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

}

15
我们从哪里获取serverCertificateData - Muruganandham K

22
据我所知,您无法轻松地直接在iOS中创建预期的公钥,您需要通过证书来完成。因此,所需的步骤类似于固定证书,但此外,您还需要从实际证书和参考证书(预期的公钥)中提取公钥。
您需要执行以下操作:
  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);
    }
 }

免责声明:此代码仅为示例,未经全面测试。 如需完整实现,请参考OWASP证书固定示例

请记住,使用SSL Kill Switch等工具始终可以避免证书固定。


2
好工作!可以在不遇到所有OpenSSL困扰的情况下进行公钥比较。 - Magoo
有人用 Swift 实现过这个吗? - vikzilla

9

1
更具体地说,证书和公钥固定的实现在 https://github.com/AFNetworking/AFNetworking/blob/master/AFNetworking/AFSecurityPolicy.m 中。 - quellish

5
您可以使用在此提到的PhoneGap(Build)插件:http://www.x-services.nl/certificate-pinning-plugin-for-phonegap-to-prevent-man-in-the-middle-attacks/734 该插件支持多个证书,因此服务器和客户端不需要同时更新。如果您的指纹每两年更改一次,那么请实现一种机制来强制客户端更新(为应用程序添加一个版本并在服务器上创建'minimalRequiredVersion' API方法。当应用程序版本过低时告知客户端更新,例如当新证书被激活时)。

3
如果您使用AFNetworking(更具体地说,是AFSecurityPolicy),并选择模式AFSSLPinningModePublicKey,则无论证书是否更改,只要公钥保持不变即可。是的,AFSecurityPolicy确实没有提供直接设置公钥的方法;您只能通过调用setPinnedCertificates来设置证书。但是,如果您查看setPinnedCertificates的实现,您会发现该框架正在从证书中提取公钥,然后比较这些公钥。
简而言之,传递证书,并不必担心它们在将来会发生变化。该框架只关心这些证书中的公钥。
以下代码适用于我。
AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager];
manager.securityPolicy = [AFSecurityPolicy policyWithPinningMode:AFSSLPinningModePublicKey];
[manager.securityPolicy setPinnedCertificates:myCertificate];

1
“...针对整个证书进行固定。这种做法会带来问题...”
“此外,谷歌每月更改证书,但保留或重新认证公共密钥。因此,证书固定将导致许多虚假警告,而公钥固定将通过密钥连续性测试。”
“我相信谷歌这样做是为了使CRL、OCSP和吊销列表易于管理,我也预计其他人会这样做。对于我的网站,我通常重新认证密钥,以确保密钥的连续性。”
“但是怎么做呢?”
证书和公钥固定。该文章讨论了这种做法,并提供了OpenSSL、Android、iOS和.Net的示例代码。至少在iOS框架中存在一个问题,可以在iOS: Provide Meaningful Error from NSUrlConnection didReceiveAuthenticationChallenge (Certificate Failure)中找到。”
此外,Peter Gutmann 在他的书 Engineering Security 中对密钥连续性和定位有很好的论述。

3
OWASP中的iOS项目示例并没有真正执行公钥固定,而是证书固定。如果你去查看他们的代码,你会发现他们执行的比较是:const BOOL equal = [cert1 isEqualToData:cert2]; 他们基本上是在比较整个证书!嗯,要么是这样,或者我漏掉了什么... - Javier Quevedo
是的,比较证书存在一个问题,即如果您更新证书,则必须使用新证书更新应用程序。相反,如果比较固定的公钥,您可以更新证书但保持公钥不变,因此无需更新应用程序。 - mskw

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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接