在提高我们正在开发的 iOS 应用程序的安全性时,我们发现需要 PIN(全部或部分)服务器的 SSL 证书以防止中间人攻击。
尽管有多种方法可以做到这一点,但当您搜索这个时,我只找到了固定整个证书的示例。这种做法会带来一个问题:一旦证书更新,您的应用程序将无法再连接。如果您选择固定公钥而不是整个证书,您会发现自己(我相信)处于同样安全的情况下,同时对服务器中的证书更新更有弹性。
但是你怎么做呢?
如果您需要知道如何从 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;
}
}
据我所知,您无法直接在 iOS 中轻松创建预期的公钥,您需要通过证书来完成。因此所需的步骤类似于固定证书,但另外您需要从实际证书和参考证书(预期的公钥)中提取公钥。
你需要做的是:
willSendRequestForAuthenticationChallenge
.一些示例代码:
(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和类似工具始终可以避免证书锁定。
SecTrustCopyPublicKey
您可以使用Security.framework的功能进行公钥 SSL 固定。请参阅AFNetworking 项目的connection:willSendRequestForAuthenticationChallenge:示例。
如果您需要 iOS 的 openSSL,请使用https://gist.github.com/foozmeat/5154962它基于 st3fan/ios-openssl,目前无法使用。
您可以使用此处提到的 PhoneGap(构建)插件:http ://www.x-services.nl/certificate-pinning-plugin-for-phonegap-to-prevent-man-in-the-middle-attacks/734
该插件支持多个证书,因此服务器和客户端不需要同时更新。如果您的指纹每(例如)2 年更改一次,则实施一种强制客户端更新的机制(向您的应用程序添加一个版本并在服务器上创建一个“minimalRequiredVersion”API 方法。如果应用程序版本是,请告诉客户端更新太低(激活新证书时的 fi)。
如果您使用 AFNetworking(更具体地说,AFSecurityPolicy),并且选择了 AFSSLPinningModePublicKey 模式,那么您的证书是否更改并不重要,只要公钥保持不变即可。是的,AFSecurityPolicy 确实没有为您提供直接设置公钥的方法;您只能通过调用来设置您的证书setPinnedCertificates
。但是,如果您查看 setPinnedCertificates 的实现,您会看到该框架正在从证书中提取公钥,然后比较这些密钥。
简而言之,把证书传进去,以后不用担心它们会变。该框架只关心这些证书中的公钥。
以下代码适用于我。
AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager];
manager.securityPolicy = [AFSecurityPolicy policyWithPinningMode:AFSSLPinningModePublicKey];
[manager.securityPolicy setPinnedCertificates:myCertificate];
...用于固定整个证书。这样的做法有问题……
此外,谷歌每月(左右)更改证书,但保留或重新认证公众。所以证书固定会导致很多虚假警告,而公钥固定会通过密钥连续性测试。
我相信谷歌这样做是为了保持 CRL、OCSP 和撤销列表的可管理性,我希望其他人也会这样做。对于我的网站,我通常会重新认证密钥,以便人们确保密钥的连续性。
但是你怎么做呢?
证书和公钥固定。本文讨论了这种做法,并提供了适用于 OpenSSL、Android、iOS 和 .Net 的示例代码。iOS 上讨论的框架中的 iOS 至少存在一个问题:Provide Meaningful Error from NSUrlConnection didReceiveAuthenticationChallenge (Certificate Failure)。
此外,Peter Gutmann 在他的《工程安全》一书中对密钥连续性和固定进行了很好的处理。
如果您使用 AFNetworking,请使用AFSecurityPolicy *policy = [AFSecurityPolicy policyWithPinningMode:AFSSLPinningModePublicKey];