52

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

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

但是你怎么做呢?

4

7 回答 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;
}

}
于 2013-03-31T10:51:01.180 回答
21

据我所知,您无法直接在 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和类似工具始终可以避免证书锁定。

于 2015-03-24T13:49:43.370 回答
9

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

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

于 2013-04-28T00:57:00.350 回答
5

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

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

于 2013-09-22T13:53:03.813 回答
3

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

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

以下代码适用于我。

AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager];
manager.securityPolicy = [AFSecurityPolicy policyWithPinningMode:AFSSLPinningModePublicKey];
[manager.securityPolicy setPinnedCertificates:myCertificate];
于 2015-04-22T22:16:05.833 回答
1

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

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

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

但是你怎么做呢?

证书和公钥固定。本文讨论了这种做法,并提供了适用于 OpenSSL、Android、iOS 和 .Net 的示例代码。iOS 上讨论的框架中的 iOS 至少存在一个问题:Provide Meaningful Error from NSUrlConnection didReceiveAuthenticationChallenge (Certificate Failure)

此外,Peter Gutmann 在他的《工程安全》一书中对密钥连续性和固定进行了很好的处理。

于 2013-04-27T04:44:04.940 回答
0

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

于 2017-02-28T03:54:09.903 回答