iOS 操作系统,已经对请求中的认证需求(有的文章中称为 挑战),做了简单的处理。当我们使用NSURLSession发起一次请求时,假如服务端返回数据要求客户端进行需求认证,不管这次需求认证是 HTTP Basic 类型(例如要求输入用户名/密码)、 ServerTrust 类型(对服务器的一个认证,单向认证),或者是 ClientCertificate 类型(对客户端的认证,双向认证),最终都会回调下面这个方法:

1
2
3
- (void)URLSession:(NSURLSession *)session
didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler;

通过实现这个代理方法,我们可以使我们的 app 支持 https 请求,并且可以进行我们自定义的认证。一般来说,对于其他类型的需求认证,我们会让系统进行默认处理,只有我们需要验证服务端证书,或者双向认证时,我们才会实现这个代理方法。包括 AF 对于 https 请求的处理,也是针对于此方法的封装。

相关基本概念

不管 https 单向认证还是双向认证,都有两个目的:

  1. 协商对称加密密钥
  2. 数据加密传输

一般来说,所谓单向认证和双向认证的区别,只是在第一步,协商对称加密密钥时,服务器对于客户端是否做证书验证。
iOS 操作系统切入点是在 协商对称加密密钥 中的 身份验证阶段,通过回调代理方法,将控制权交给 app 。

代理方法传入的参数中,包含了这次认证需求的类型,包括 对服务器的认证(单向认证ServerTrust) 或者 对客户端证书的需求(ClientCertificate) 。我们在代理方法中可以验证服务器的证书,并通过调用传入的 completionBlock 来传入系统需要的参数。

单向认证

先看一下单向认证的流程:

  1. 客户端 A 向 服务端 B 发起请求,传输了 A 所支持的 SSL/TLS 协议版本列表、支持的对称加密算法列表、随机数 a。
  2. B 选择一个 SSL/TLS 协议版本、对称加密算法、包含自己公钥的证书、随机数 b 发送给 A。
  3. A 对 B 发送的 证书 进行验证,包括 域名是否一致、证书是否过期吊销、证书是否可信。对于证书是否可信,设备中一般会有一个证书信任列表,存储了这些证书机构的公钥,证书中包含了对 B 公钥的加密,和机构对信息的签名,设备用机构的公钥进行验签,验证通过则代表 B 公钥的可信度。这时候 A 生成一个随机数,使用 B 公钥进行加密,传输给 B 。
  4. 最后一步,B 用自己的私钥解密,得到 c ,这时候A he B 都得到了 a b c,使用 a b c 生成对称加密的密钥,后续中就使用这个密钥进行数据加密传输。

在第 3 步,验证证书的时候,系统会回调代理方法,假如我们服务器的证书,是通过正规渠道申请的,即使不实现这个代理方法,系统也会对证书进行验证。
当我们使用自签证书时,系统的证书信任列表中没有这个证书相关信息,我们就必须实现这个代理方法,否则会报错。

双向认证

区别在于服务端会对客户端进行验证,主要是第 3 步 和 第 4 步

  1. ~
  2. ~
  3. A 对 B 的证书进行验证通过后,将信息使用自己的私钥进行加密,并将自己的证书一起传输到 B。
  4. B 对 A 发送过来的证书进行验证,通过后取出 A 的公钥,对信息进行解密,得到 c ,生成对称加密密钥。

具体实现

单向认证

具体的代码如下,这里使用 AF ,其实都一样, AF 只不过做了简单的封装。

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
- (void)URLSession:(NSURLSession *)session
didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler
{
NSURLSessionAuthChallengeDisposition disposition = NSURLSessionAuthChallengePerformDefaultHandling;
__block NSURLCredential *credential = nil;

if (self.sessionDidReceiveAuthenticationChallenge) {
/// 允许使用者自定义处理方式,双向认证的时候会用到。
disposition = self.sessionDidReceiveAuthenticationChallenge(session, challenge, &credential);
} else {
/// 判断这次认证需求的类型是否是 https 中的 对服务端认证,不是的话让系统进行默认处理。
if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
if ([self.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 {
disposition = NSURLSessionAuthChallengePerformDefaultHandling;
}
}

if (completionHandler) {
completionHandler(disposition, credential);
}
}

我们再查看一下验证服务端证书的具体方法实现,首先要清楚的是,证书验证包括域名一致验证,证书有效验证,公钥验证。

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
70
71
72
73
74
75
76
77
78
79
80
81
- (BOOL)evaluateServerTrust:(SecTrustRef)serverTrust
forDomain:(NSString *)domain
{
if (domain && self.allowInvalidCertificates && self.validatesDomainName && (self.SSLPinningMode == AFSSLPinningModeNone || [self.pinnedCertificates count] == 0)) {
// https://developer.apple.com/library/mac/documentation/NetworkingInternet/Conceptual/NetworkingTopics/Articles/OverridingSSLChainValidationCorrectly.html
// According to the docs, you should only trust your provided certs for evaluation.
// Pinned certificates are added to the trust. Without pinned certificates,
// there is nothing to evaluate against.
//
// From Apple Docs:
// "Do not implicitly trust self-signed certificates as anchors (kSecTrustOptionImplicitAnchors).
// Instead, add your own (self-signed) CA certificate to the list of trusted anchors."
NSLog(@"In order to validate a domain name for self signed certificates, you MUST use pinning.");
/// 假如我们配置的需要验证域名,那么 SSLPinningNode 就不能设置为 None,必须设置为其他类型。
return NO;
}
/// 是否验证域名,假如验证域名,将域名添加到验证条件中。
NSMutableArray *policies = [NSMutableArray array];
if (self.validatesDomainName) {
[policies addObject:(__bridge_transfer id)SecPolicyCreateSSL(true, (__bridge CFStringRef)domain)];
} else {
[policies addObject:(__bridge_transfer id)SecPolicyCreateBasicX509()];
}

SecTrustSetPolicies(serverTrust, (__bridge CFArrayRef)policies);
/// 假如验证模式设置为 None ,代表无条件信任服务端,直接返回证书有效验证结果,假如我们选择允许无效证书,直接返回 YES,通过这次验证。
if (self.SSLPinningMode == AFSSLPinningModeNone) {
return self.allowInvalidCertificates || AFServerTrustIsValid(serverTrust);
} else if (!AFServerTrustIsValid(serverTrust) && !self.allowInvalidCertificates) {
/// 假如证书无效,且不允许无效证书,返回 NO。
return NO;
}
/// 针对不同验证模式进行不同处理
switch (self.SSLPinningMode) {
case AFSSLPinningModeNone:
/// 不验证,上面已经进行了处理。
default:
return NO;
case AFSSLPinningModeCertificate: {
/// 验证证书模式,对证书中的全部内容进行验证,会验证证书有效性,证书是否一致。
NSMutableArray *pinnedCertificates = [NSMutableArray array];
for (NSData *certificateData in self.pinnedCertificates) {
[pinnedCertificates addObject:(__bridge_transfer id)SecCertificateCreateWithData(NULL, (__bridge CFDataRef)certificateData)];
}
/// 设置比对锚点证书数组
SecTrustSetAnchorCertificates(serverTrust, (__bridge CFArrayRef)pinnedCertificates);
/// 验证证书有效性
if (!AFServerTrustIsValid(serverTrust)) {
return NO;
}

// obtain the chain after being validated, which *should* contain the pinned certificate in the last position (if it's the Root CA)
/// 验证是否证书一致,我们设置的证书数组中,包含其中一个即可。
NSArray *serverCertificates = AFCertificateTrustChainForServerTrust(serverTrust);

for (NSData *trustChainCertificate in [serverCertificates reverseObjectEnumerator]) {
if ([self.pinnedCertificates containsObject:trustChainCertificate]) {
return YES;
}
}

return NO;
}
case AFSSLPinningModePublicKey: {
/// 验证公钥模式,只进行证书的公钥一致性验证。
NSUInteger trustedPublicKeyCount = 0;
NSArray *publicKeys = AFPublicKeyTrustChainForServerTrust(serverTrust);
/// 获取证书中的公钥数组,与自己设置的证书中的公钥数组进行比对,有一个相同即可。
for (id trustChainPublicKey in publicKeys) {
for (id pinnedPublicKey in self.pinnedPublicKeys) {
if (AFSecKeyIsEqualToKey((__bridge SecKeyRef)trustChainPublicKey, (__bridge SecKeyRef)pinnedPublicKey)) {
trustedPublicKeyCount += 1;
}
}
}
return trustedPublicKeyCount > 0;
}
}

return NO;
}

可以看到,我们使用 AF 进行 https 请求时。

  1. 假如我们想无条件的信任服务器的证书,就将 manager.securityPolicy.SSLPinningMode = AFSSLPinningModeNonemanager.securityPolicy.allowInvalidCertificates = NO ,这样服务端要求进行 服务端证书认证 时,是直接信任的。
    直接信任服务端的证书,会有 中间人攻击 的隐患,我们使用 Charles 进行 HTTPS 抓包时,对于 客户端 ,Charles 是服务端,对于 服务端,Charles 是客户端,客户端收到的 证书认证中,证书 是 Charles 的证书,系统会通过认证。

  2. 假如我们需要验证服务端证书,就在开发期间导入 证书,可以选择证书验证模式或者 公钥验证模式,AF 自动去帮我们验证证书或者公钥一致性。

双向认证

AF 中,似乎只默认处理了单向认证也就是针对于 服务端的认证需求,但是也允许我们自己处理认证需求,通过设置 manager.sessionDidReceiveAuthenticationChallenge ,具体代码如下:

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
70
71
72
73
74
75
76
77
/// 设置 manager 的 认证需求处理 block
@weakify(manager);
@weakify(self);
[manager setSessionDidReceiveAuthenticationChallengeBlock:^NSURLSessionAuthChallengeDisposition(NSURLSession * _Nonnull session, NSURLAuthenticationChallenge * _Nonnull challenge, NSURLCredential *__autoreleasing _Nullable * _Nullable credential) {
NSURLSessionAuthChallengeDisposition disposition = NSURLSessionAuthChallengePerformDefaultHandling;
if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
/// 针对于服务端 证书验证,还是走 AF 的配置进行处理。
if ([manager_weak_.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 if([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodClientCertificate])
{
/// 针对于客户端的认证需求,通过事先导入的 p12 文件生成证书,传给系统。
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_weak_ 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;
}
}

}
else
{
disposition = NSURLSessionAuthChallengePerformDefaultHandling;
}
return disposition;
}];

/// 根据 NSData 生成证书的方法
+ (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;
}

双向认证中,我们依然要对服务端证书进行认证,以及对其他的 认证需求类型,指定进行系统默认的处理;但是主要的是针对客户端认证代码,利用我们实现导入的证书文件,生成了一个证书交给了系统,让系统为我们进行进一步的处理。