简体   繁体   English

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

[英]How to pin the Public key of a certificate on iOS

While improving the security of an iOS application that we are developing, we found the need to PIN (the entire or parts of) the SSL certificate of server to prevent man-in-the-middle attacks. 在提高我们正在开发的iOS应用程序的安全性的同时,我们发现需要PIN(全部或部分)服务器的SSL证书来防止中间人攻击。

Even though there are various approaches to do this, when you searching for thisI only found examples for pinning the entire certificate. 即使有各种方法可以做到这一点,当你搜索这个时我只找到了固定整个证书的例子。 Such practice poses a problem: As soon as the certificate is updated, your application will not be able to connect anymore. 这种做法带来了一个问题:一旦证书更新,您的应用程序将无法再连接。 If you choose to pin the public key instead of the entire certificate you will find yourself (I believe) in an equally secure situation, while being more resilient to certificate updates in the server. 如果您选择固定公钥而不是整个证书,您将发现自己(我相信)处于同样安全的情况,同时对服务器中的证书更新更具弹性。

But how do you do this? 但是你怎么做的?

In case you are in need of knowing how to extract this information from the certificate in your iOS code, here you have one way to do it. 如果您需要知道如何从iOS代码中的证书中提取此信息,这里有一种方法可以执行此操作。

First of all add the security framework. 首先添加安全框架。

#import <Security/Security.h>

The add the openssl libraries. 添加openssl库。 You can download them from https://github.com/st3fan/ios-openssl 您可以从https://github.com/st3fan/ios-openssl下载它们

#import <openssl/x509.h>

The NSURLConnectionDelegate Protocol allows you to decide whether the connection should be able to respond to a protection space. NSURLConnectionDelegate协议允许您决定连接是否应该能够响应保护空间。 In a nutshell, this is when you can have a look at the certificate that is coming from the server, and decide to allow the connection to proceed or to cancel. 简而言之,这是您可以查看来自服务器的证书,并决定允许连接继续或取消。 What you want to do here is compare the certificates public key with the one you've pinned. 您要在此处执行的操作是将证书公钥与您固定的证书进行比较。 Now the question is, how do you get such public key? 现在的问题是,你如何得到这样的公钥? Have a look at the following code: 看看下面的代码:

First get the certificate in X509 format (you will need the ssl libraries for this) 首先获得X509格式的证书(你需要ssl库)

const unsigned char *certificateDataBytes = (const unsigned char *)[serverCertificateData bytes];
X509 *certificateX509 = d2i_X509(NULL, &certificateDataBytes, [serverCertificateData length]);

Now we will prepare to read the public key data 现在我们将准备读取公钥数据

ASN1_BIT_STRING *pubKey2 = X509_get0_pubkey_bitstr(certificateX509);

NSString *publicKeyString = [[NSString alloc] init];    

At this point you can iterate through the pubKey2 string and extract the bytes in HEX format into a string with the following loop 此时,您可以遍历pubKey2字符串并以十六进制格式将字节提取为具有以下循环的字符串

 for (int i = 0; i < pubKey2->length; i++)
{
    NSString *aString = [NSString stringWithFormat:@"%02x", pubKey2->data[i]];
    publicKeyString = [publicKeyString stringByAppendingString:aString];
}

Print the public key to see it 打印公钥以查看它

 NSLog(@"%@", publicKeyString);

The complete code 完整的代码

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

}

As far as I can tell you cannot easily create the expected public key directly in iOS, you need to do it via a certificate. 据我所知,您不能直接在iOS中轻松创建预期的公钥,您需要通过证书来完成。 So the steps needed are similar to pinning the certificate, but additionally you need to extract the public key from the actual certificate, and from a reference certificate (the expected public key). 因此,所需的步骤与固定证书类似,但您还需要从实际证书和参考证书(预期的公钥)中提取公钥。

What you need to do is: 你需要做的是:

  1. Use a NSURLConnectionDelegate to retrieve the data, and implement willSendRequestForAuthenticationChallenge . 使用NSURLConnectionDelegate检索数据,并实现willSendRequestForAuthenticationChallenge
  2. Include a reference certificate in DER format. 包括DER格式的参考证书。 In the example I've used a simple resource file. 在示例中,我使用了一个简单的资源文件。
  3. Extract the public key presented by the server 提取服务器提供的公钥
  4. Extract the public key from your reference certificate 从参考证书中提取公钥
  5. Compare the two 比较两者
  6. If they match, continue with the regular checks (hostname, certificate signing, etc) 如果匹配,继续进行常规检查(主机名,证书签名等)
  7. If they don't match, fail. 如果它们不匹配,则失败。

Some example code: 一些示例代码:

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

Disclaimer: this is example code only, and not thoroughly tested. 免责声明:这只是示例代码,未经过全面测试。 For a full implementation start with the certificate pinning example by OWASP . 有关完整实现,请参阅OWASP证书锁定示例

And remember that certificate pinning can always be avoided using SSL Kill Switch and similar tools. 请记住,使用SSL Kill Switch和类似工具始终可以避免证书固定。

You can do public key SSL pinning using the SecTrustCopyPublicKey function of the Security.framework. 您可以使用Security.framework的SecTrustCopyPublicKey函数执行公钥SSL固定。 See an example at connection:willSendRequestForAuthenticationChallenge: of the AFNetworking project. 请参阅AFNetworking项目的连接示例:willSendRequestForAuthenticationChallenge : .

If you need openSSL for iOS, use https://gist.github.com/foozmeat/5154962 It's based on st3fan/ios-openssl, which currently doesn't work. 如果您需要iOS版openSSL,请使用https://gist.github.com/foozmeat/5154962它基于st3fan / ios-openssl,目前无效。

You could use the PhoneGap (Build) plugin mentioned here: http://www.x-services.nl/certificate-pinning-plugin-for-phonegap-to-prevent-man-in-the-middle-attacks/734 你可以使用这里提到的PhoneGap(Build)插件: http//www.x-services.nl/certificate-pinning-plugin-for-phonegap-to-prevent-man-in-the-middle-attacks/734

The plugin supports multiple certificates, so the server and client don't need to be updated at the same time. 该插件支持多个证书,因此不需要同时更新服务器和客户端。 If your fingerprint changes every (say) 2 year, then implement a mechanism for forcing the clients to update (add a version to your app and create a 'minimalRequiredVersion' API method on the server. Tell the client to update if the app version is too low (fi when the new certificate is activate). 如果您的指纹每隔(比如说)2年发生变化,那么实施一种强制客户端更新的机制(在您的应用中添加一个版本并在服务器上创建'minimalRequiredVersion'API方法。如果应用版本是,请告诉客户更新太低(激活新证书时为fi)。

If you use AFNetworking (more specifically, AFSecurityPolicy), and you choose the mode AFSSLPinningModePublicKey, it doesn't matter if your certificates change or not, as long as the public keys stay the same. 如果您使用AFNetworking(更具体地说,AFSecurityPolicy),并且您选择模式AFSSLPinningModePublicKey,则只要公钥保持不变,您的证书是否更改无关紧要。 Yes, it is true that AFSecurityPolicy doesn't provide a method for you to directly set your public keys; 是的,AFSecurityPolicy确实没有为您提供直接设置公钥的方法; you can only set your certificates by calling setPinnedCertificates . 您只能通过调用setPinnedCertificates来设置证书。 However, if you look at the implementation of setPinnedCertificates, you'll see that the framework is extracting the public keys from the certificates and then comparing the keys. 但是,如果查看setPinnedCertificates的实现,您将看到框架从证书中提取公钥,然后比较密钥。

In short, pass in the certificates, and don't worry about them changing in the future. 简而言之,通过证书,不要担心它们将来会发生变化。 The framework only cares about the public keys in those certificates. 该框架仅关注这些证书中的公钥。

The following code works for me. 以下代码适用于我。

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

...for pinning the entire certificate. ...用于固定整个证书。 Such practice poses a problem... 这种做法造成了一个问题......

Also, Google changes the certificate monthly (or so) but retains or re-certifies the public. 此外,Google每月(或左右)更改证书,但保留或重新认证公众。 So certificate pinning will result in a lot of spurious warnings, while public key pinning will pass key continuity tests. 因此,证书锁定将导致大量虚假警告,而公钥锁定将通过密钥连续性测试。

I believe Google does it to keep CRLs, OCSP and Revocation Lists manageable, and I expect others will do it also. 我相信谷歌可以保持CRL,OCSP和撤销列表的可管理性,我希望其他人也会这样做。 For my sites, I usually re-certify the keys so folks to ensure key continuity. 对于我的网站,我通常会重新认证密钥,以便确保密钥连续性。

But how do you do this? 但是你怎么做的?

Certificate and Public Key Pinning . 证书和公钥固定 The article discusses the practice and offers sample code for OpenSSL, Android, iOS, and .Net. 本文讨论了这种做法,并提供了OpenSSL,Android,iOS和.Net的示例代码。 There is at least one problem with iOS incumbent to the framework discussed at iOS: Provide Meaningful Error from NSUrlConnection didReceiveAuthenticationChallenge (Certificate Failure) . iOS在iOS中讨论的框架至少存在一个问题:从NSUrlConnection didReceiveAuthenticationChallenge(证书失败)提供有意义的错误

Also, Peter Gutmann has a great treatment of key continuity and pinning in his book Engineering Security . 此外,彼得古特曼在他的“ 工程安全”一书中对关键的连续性和钉扎有很好的处理。

Here the Swifty answer. 这里是Swifty的回答。 Save the certificate (as .cer file) of your website in the main bundle. 在主包中保存您网站的证书(作为.cer文件)。 Then use this URLSessionDelegate method: 然后使用 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))
}

You can get the .cer file with Chrome like this . 您可以像这样使用Chrome获取.cer文件。

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

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM