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

时间:2013-03-31 10:51:01

标签: iphone ios ipad security ssl

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

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

但你是怎么做到的?

8 个答案:

答案 0 :(得分: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字符串并以十六进制格式将字节提取为具有以下循环的字符串

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

}

答案 1 :(得分:21)

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

您需要做的是:

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

     (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和类似工具可以始终避免使用证书固定。

答案 2 :(得分:9)

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

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

答案 3 :(得分:5)

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

该插件支持多个证书,因此不需要同时更新服务器和客户端。如果您的指纹每隔(比如说)2年发生变化,那么实施一种强制客户端更新的机制(在您的应用中添加一个版本并在服务器上创建'minimalRequiredVersion'API方法。如果应用版本是,请告诉客户更新太低(激活新证书时为fi)。

答案 4 :(得分:3)

如果您使用AFNetworking(更具体地说,AFSecurityPolicy),并选择模式AFSSLPinningModePublicKey,只要公钥保持不变,您的证书是否更改就无关紧要。是的,AFSecurityPolicy确实没有为您提供直接设置公钥的方法;您只能通过致电setPinnedCertificates来设置证书。但是,如果查看setPinnedCertificates的实现,您将看到框架正在从证书中提取公钥,然后比较密钥。

简而言之,通过证书,并且不用担心将来会发生变化。该框架仅关注这些证书中的公钥。

以下代码适用于我。

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

答案 5 :(得分:2)

这里是Swifty的回答。在主包中保存您网站的证书(作为.cer文件)。然后使用this 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 this获取.cer文件。

答案 6 :(得分:1)

  

...用于固定整个证书。这种做法带来了一个问题...

此外,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中对关键连续性和钉扎有很好的处理。

答案 7 :(得分:0)

如果您使用AFNetworking,请使用querySelectorAll