iOS https防止中间人攻击

“公钥” 和 “私钥”

首先要明白两个概念“公钥”和“私钥”,
“公钥” 可以理解成锁头,“私钥” 就是钥匙。我们可以把 “公钥” 给客户端用来加密数据,当数据传回来时再用私钥解开加密就可以看到真正的内容了。
https数字证书:里面包含公钥,一般签发证书时会得到一个与公钥配对的私钥。证书会在通信过程中传给客户端,客户端也可以放一个,用来对比和服务端传过来的是不是一致

https通信认证的具体过程

单向认证:

  1. 客户端发起https请求:比如用户在浏览器里输入一个https网址,然后连接到server的443端口。
  2. 服务器响应请求,并把服务器证书传给客户端
  3. 客户端收到证书,并和本地证书进行对比,如果不一致,那么断开连接。如果通过进行第4步
  4. 客户端随机生成一个密钥key(用来后面通信的对称加密),用证书中的公钥加密该key,并传回服务端
  5. 服务端接收到key加密后的数据,然后用私钥解出key
  6. 服务端和客户端现在都指定这个key为后面通信对称加密的密钥,握手结束。
  7. 服务端和客户端开始真正通信,通信数据由key对称加密。
    upload successful

双向认证

双向认证比单向认证多了一步,就是服务器要认证客户端,按照百度百科上的步骤,客户端应该有一个由CA(或正规机构)签发的p12证书,和CA根证书(签名的p12就是由这个签名的)

//上面的CA根证书和p12证书   其实都可以自签的

材料:买来的服务器证书server.cer(客户端要放一个,用来验证服务端),客户端,服务器端,CA的根证书(放到服务器中,用来验证客户端的证书),p12证书(用来放到客户端,网络请求的时候会传给服务端)。

步骤:就是在上面第4步的时候,要用p12文件来对一段数据进行签名,然后把签名和p12证书,加密的对称密钥(上面的说过的)传给服务器,然后服务器接到以后,会用CA根证书(或自签的根证书)来对证书和签名数据进行验证,如果正确,通讯继续,否则,断开连接。

其他的都是一样的。

中间人攻击:

upload successful

过程分析

  • 客户端首先要向远程的服务器发送建立连接的请求,并带有自己的支持的加解密的方式级别,这个过程经过了中间人的窃听,中间人把消息修改后发给了真正的目的地——服务端
  • 服务端收到了要建立https链接的请求后,会发送当时从证书签发机构签发的公钥证书。这个过程中中间人又窃听了,然后中间人替换上自己的证书后又转发给了客户端。
  • 客户端收到了中间人发过来的公钥证书,验证证书的真伪,并产生随机的对称加密的密钥,用中间人发的公钥加密后发给了中间人。由于刚才客户端收到的公钥证书本身就是中间人产生的,所以中间人用相应的私钥就解开了,拿到了客户端产生的那个随机产生的对称加密密钥。中间人再用刚才服务端返回的公钥证书加密这个客户端产生的用来对称加密的密钥,发给服务端。
  • 服务端收到了当时用自己下发的公钥的证书加密的对称加密密钥,用自己的私钥解密,也得到了对称加密的密钥。
  • 以后的通信都使用这个对称加密的密钥加密了。因为客户端,中间人,服务端都有了这个对称加密的密钥,所以都可以用此解密通信的内容。(上面的步骤是穿插了HTTPS建立握手过程和中间人的作用介绍的,属于简洁介绍,明白原理就可以了)。
    上面有几个字“验证证书的真伪”加粗了,其实一般来说这个过程应该是安全的,因为一般的证书都是由操作系统来管理(Firefox自己管理)的,所以只要操作系统没有证书链验证等方面的bug是没有什么问题的,但是为了抓包其实我们是在操作系统中导入了中间人的CA,这样中间人下发的公钥证书就可以被认为是合法的,可以通过验证的(中间人既承担了办法了证书,又承担了验证证书,能不通过验证嘛)。

解决办法

客户端为了解决这个问题,最好的方式其实就是内嵌证书,比对一下这个证书到底是不是自己真正的“服务端”发来的,而不是中间被替换了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 单向认证只需这段代码就行
....
shareManager = [AFHTTPSessionManager manager];
shareManager.securityPolicy = [self customSecurityPolicy];
...

+ (AFSecurityPolicy*)customSecurityPolicy
{
// /先导入证书
NSString *cerPath = [[NSBundle mainBundle] pathForResource:@"server" ofType:@"cer"];//证书的路径
NSData *certData = [NSData dataWithContentsOfFile:cerPath];

// AFSSLPinningModeCertificate 使用证书验证模式
AFSecurityPolicy *securityPolicy = [AFSecurityPolicy policyWithPinningMode:AFSSLPinningModeCertificate];

// allowInvalidCertificates 是否允许无效证书(也就是自建的证书),默认为NO
// 如果是需要验证自建证书,需要设置为YES
securityPolicy.allowInvalidCertificates = YES;

//validatesDomainName 是否需要验证域名,默认为YES;
//假如证书的域名与你请求的域名不一致,需把该项设置为NO;如设成NO的话,即服务器使用其他可信任机构颁发的证书,也可以建立连接,这个非常危险,建议打开。
//置为NO,主要用于这种情况:客户端请求的是子域名,而证书上的是另外一个域名。因为SSL证书上的域名是独立的,假如证书上注册的域名是www.google.com,那么mail.google.com是无法验证通过的;当然,有钱可以注册通配符的域名*.google.com,但这个还是比较贵的。
//如置为NO,建议自己添加对应域名的校验逻辑。
securityPolicy.validatesDomainName = NO;

securityPolicy.pinnedCertificates = @[certData];

return securityPolicy;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
/// 双向认证需添加这些代码

[shareManager setSessionDidReceiveAuthenticationChallengeBlock:^NSURLSessionAuthChallengeDisposition(NSURLSession *session, NSURLAuthenticationChallenge *challenge, NSURLCredential *__autoreleasing *_credential) {
NSURLSessionAuthChallengeDisposition disposition = NSURLSessionAuthChallengePerformDefaultHandling;
__autoreleasing NSURLCredential *credential = nil;
if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
if ([shareManager.securityPolicy evaluateServerTrust:challenge.protectionSpace.serverTrust forDomain:challenge.protectionSpace.host]) {
credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
if(credential) {
disposition =NSURLSessionAuthChallengeUseCredential;
} else {
disposition =NSURLSessionAuthChallengePerformDefaultHandling;
}
} else {
disposition = NSURLSessionAuthChallengeCancelAuthenticationChallenge;
}
} else {
SecIdentityRef identity = NULL;
SecTrustRef trust = NULL;
NSString *p12 = [[NSBundle mainBundle] pathForResource:@"client"ofType:@"p12"];
NSFileManager *fileManager =[NSFileManager defaultManager];

if(![fileManager fileExistsAtPath:p12])
{
NSLog(@"client.p12:not exist");
}
else
{
NSData *PKCS12Data = [NSData dataWithContentsOfFile:p12];

if ([[self class] extractIdentity:&identity andTrust:&trust fromPKCS12Data:PKCS12Data])
{
SecCertificateRef certificate = NULL;
SecIdentityCopyCertificate(identity, &certificate);
const void*certs[] = {certificate};
CFArrayRef certArray =CFArrayCreate(kCFAllocatorDefault, certs,1,NULL);
credential =[NSURLCredential credentialWithIdentity:identity certificates:(__bridge NSArray*)certArray persistence:NSURLCredentialPersistencePermanent];
disposition =NSURLSessionAuthChallengeUseCredential;
}
}
}
*_credential = credential;
return disposition;
}]

+ (BOOL)extractIdentity:(SecIdentityRef*)outIdentity andTrust:(SecTrustRef *)outTrust fromPKCS12Data:(NSData *)inPKCS12Data {
OSStatus securityError = errSecSuccess;
//client certificate password
NSDictionary*optionsDictionary = [NSDictionary dictionaryWithObject:@"your p12 file pwd"
forKey:(__bridge id)kSecImportExportPassphrase];

CFArrayRef items = CFArrayCreate(NULL, 0, 0, NULL);
securityError = SecPKCS12Import((__bridge CFDataRef)inPKCS12Data,(__bridge CFDictionaryRef)optionsDictionary,&items);

if(securityError == 0) {
CFDictionaryRef myIdentityAndTrust =CFArrayGetValueAtIndex(items,0);
const void*tempIdentity =NULL;
tempIdentity= CFDictionaryGetValue (myIdentityAndTrust,kSecImportItemIdentity);
*outIdentity = (SecIdentityRef)tempIdentity;
const void*tempTrust =NULL;
tempTrust = CFDictionaryGetValue(myIdentityAndTrust,kSecImportItemTrust);
*outTrust = (SecTrustRef)tempTrust;
} else {
NSLog(@"Failedwith error code %d",(int)securityError);
return NO;
}
return YES;
}

参考