HTTPS Trust CA in iOS

Apple ATS

Apple ATS

为什么要支持HTTPS

苹果公司要求开发者限期支持ATS

在WWDC 2016开发者大会上,苹果宣布了一个最后期限:到2017年1月1日 App Store中的所有应用都必须启用 App Transport Security安全功能。App Transport Security(ATS)是苹果在iOS 9中引入的一项隐私保护功能,屏蔽明文HTTP资源加载,连接必须经过更安全的HTTPS。苹果目前允许开发者暂时可以继续使用HTTP连接,但到年底所有官方商店的应用都必须强制性使用ATS。

以下是开发者网站公告原文:

应用传输安全协议是与iOS9和OS X 10.11一同发布的,该协议需要应用程序通过HTTPS使用安全的网络连接,以提高用户的数据和隐私安全。

在2016年WWDC上我们宣布在今年年底之前,提交到App Store的应用程序必须支持应用传输安全协议。为了给你们额外的时间去准备,这个截止日期已被延长,当新的截止日期确定的时候,我们将及时提供相关信息。

2016年12月21日苹果更新了截止日期,宣布延期执行ATS支持要求Supporting App Transport Security

开发者网站截图

此举为开发者提供了更多时间来做适配和支持。然而,对于iOS开发者来说,尽早解决HTTPS请求的问题仍为上策。

ATS对HTTPS证书的要求

启用ATS必须符合以下标准,不满足条件的HTTPS证书,ATS都会拒绝连接:

  • 服务器所有的连接使用TLS1.2以上版本
  • HTTPS证书必须使用SHA256以上哈希算法签名
  • HTTPS证书必须使用RSA 2048位或ECC 256位以上公钥算法
  • 使用前向加密技术

此外,苹果ATS支持CT证书透明,要求开发者使用支持CT证书透明度的SSL证书,确保SSL证书合法透明,防止中间人攻击。

HTTPS以及SSL/TSL

首先来分析一下什么是HTTPS以及了解HTTPS对于iOS开发者的意义。

什么是SSL

SSL(Secure Sockets Layer, 安全套接字层),因为原先互联网上使用的 HTTP 协议是明文的,存在很多缺点,比如传输内容会被偷窥(嗅探)和篡改。 SSL 协议的作用就是在传输层对网络连接进行加密。

何为TLS

到了1999年,SSL 因为应用广泛,已经成为互联网上的事实标准。IETF 就在那年把 SSL 标准化。标准化之后的名称改为 TLS(Transport Layer Security,传输层安全协议)。SSL与TLS可以视作同一个东西的不同阶段

HTTPS

简单来说,HTTPS = HTTP + SSL/TLS, 也就是 HTTP over SSL 或 HTTP over TLS,这是后面加 S 的由来 。

HTTPS和HTTP异同

HTTP和HTTPS使用的是完全不同的连接方式,用的端口也不一样,前者是80,后者是443。HTTP的连接很简单,是无状态的;HTTPS协议是由SSL+HTTP协议构建的可进行加密传输、身份认证的网络协议,比HTTP协议安全。

发送HTTPS请求信任SSL证书和自签名证书

这里有三种情况可以解决问题,且各有优缺点,开发者可根据实际情况进行选择和处理。

系统方法信任SSL证书

如果你的app服务端安装的是SLL颁发的CA,可以使用系统方法直接实现信任SSL证书,关于Apple对SSL证书的要求请参考:苹果官方文档CertKeyTrustProgGuide

这种方式不需要在Bundle中引入CA文件,可以交给系统去判断服务器端的证书是不是SSL证书,验证过程也不需要我们去具体实现。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14



NSURL *URL = [NSURL URLWithString:URLString];

NSURLRequest *request = [[NSURLRequest alloc] initWithURL:URL cachePolicy:NSURLRequestReloadIgnoringLocalCacheData timeoutInterval:10];

//创建同步连接

NSError *error = nil;

NSData *receivedData = [NSURLConnection sendSynchronousRequest:request returningResponse:nil error:&error];

NSString *receivedInfo = [[NSString alloc] initWithData:receivedData encoding:NSUTF8StringEncoding];

当然,如果你需要同时信任SSL证书和自签名证书的话还是需要在代码中实现CA的验证,这种情况在后面会提到。

基于AFNetWorking的SSL特定服务器证书信任处理

重写AFNetWorking的customSecurityPolicy方法,这里我创建了一个HttpRequest类,分别对GET和POST方法进行了封装,以GET方法为例:

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





+ (void)get:(NSString *)url params:(NSDictionary *)params success:(void (^)(id))success failure:(void (^)(NSError *))failure {

// 1.获得请求管理者

AFHTTPRequestOperationManager *mgr = [AFHTTPRequestOperationManager manager];

// 2.申明返回的结果是text/html类型

mgr.responseSerializer = [AFHTTPResponseSerializer serializer];

// 3.设置超时时间为10s

mgr.requestSerializer.timeoutInterval = 10;



// 加上这行代码,https ssl 验证。

if(openHttpsSSL) {

[mgr setSecurityPolicy:[self customSecurityPolicy]];

}



// 4.发送GET请求

[mgr GET:url parameters:params success:^(AFHTTPRequestOperation *operation, id responseObj){

if (success) {

success(responseObj);

}

} failure:^(AFHTTPRequestOperation *operation, NSError *error) {

if (error) {

failure(error);

}

}];

}
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

+ (AFSecurityPolicy*)customSecurityPolicy {

// /先导入证书

NSString *cerPath = [[NSBundle mainBundle] pathForResource:certificate 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;

}

其中的cerPath就是app bundle中证书路径,certificate为证书名称的宏,仅支持cer格式,securityPolicy的相关配置尤为重要,请仔细阅读customSecurityPolicy方法并根据实际情况设置其属性。

这样,就能够在AFNetWorking的基础上使用HTTPS协议访问特定服务器,但是不能信任根证书的CA文件,因此这种方式存在风险,读取pinnedCertificates中的证书数组的时候有可能失败,如果证书不符合,certData就会为nil。

更改系统方法,发送异步NSURLConnection请求

NSURLConnection的常规方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
- (void)getDataWithURLRequest {

//connection

NSString *urlStr = @"https://developer.apple.com/cn/";

NSURL *url = [NSURL URLWithString:urlStr];

NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url cachePolicy:NSURLRequestUseProtocolCachePolicy timeoutInterval:10];

NSURLConnection *connection = [[NSURLConnection alloc]initWithRequest:request delegate:self];

[connection start];

}

重点在于处理NSURLConnection的didReceiveAuthenticationChallenge代理方法,对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
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
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
- (BOOL)connection:(NSURLConnection *)connection canAuthenticateAgainstProtectionSpace:(NSURLProtectionSpace *)protectionSpace {



return [protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust];

}



- (void)connection:(NSURLConnection *)connection didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge {



/*

//直接验证服务器是否被认证(serverTrust),这种方式直接忽略证书验证,直接建立连接,但不能过滤其它URL连接,可以理解为一种折衷的处理方式,实际上并不安全,因此不推荐。

SecTrustRef serverTrust = [[challenge protectionSpace] serverTrust];

return [[challenge sender] useCredential: [NSURLCredential credentialForTrust: serverTrust]

forAuthenticationChallenge: challenge];

*/

if ([[[challenge protectionSpace] authenticationMethod] isEqualToString: NSURLAuthenticationMethodServerTrust]) {

do

{

SecTrustRef serverTrust = [[challenge protectionSpace] serverTrust];

NSCAssert(serverTrust != nil, @"serverTrust is nil");

if(nil == serverTrust)

break; /* failed */

/**

* 导入多张CA证书(Certification Authority,支持SSL证书以及自签名的CA)

*/

NSString *cerPath = [[NSBundle mainBundle] pathForResource:@"cloudwin" ofType:@"cer"];//自签名证书

NSData* caCert = [NSData dataWithContentsOfFile:cerPath];



NSString *cerPath2 = [[NSBundle mainBundle] pathForResource:@"apple" ofType:@"cer"];//SSL证书

NSData * caCert2 = [NSData dataWithContentsOfFile:cerPath2];



NSCAssert(caCert != nil, @"caCert is nil");

if(nil == caCert)

break; /* failed */



NSCAssert(caCert2 != nil, @"caCert2 is nil");

if (nil == caCert2) {

break;

}



SecCertificateRef caRef = SecCertificateCreateWithData(NULL, (__bridge CFDataRef)caCert);

NSCAssert(caRef != nil, @"caRef is nil");

if(nil == caRef)

break; /* failed */



SecCertificateRef caRef2 = SecCertificateCreateWithData(NULL, (__bridge CFDataRef)caCert2);

NSCAssert(caRef2 != nil, @"caRef2 is nil");

if(nil == caRef2)

break; /* failed */



NSArray *caArray = @[(__bridge id)(caRef),(__bridge id)(caRef2)];



NSCAssert(caArray != nil, @"caArray is nil");

if(nil == caArray)

break; /* failed */



OSStatus status = SecTrustSetAnchorCertificates(serverTrust, (__bridge CFArrayRef)caArray);

NSCAssert(errSecSuccess == status, @"SecTrustSetAnchorCertificates failed");

if(!(errSecSuccess == status))

break; /* failed */



SecTrustResultType result = -1;

status = SecTrustEvaluate(serverTrust, &result);

if(!(errSecSuccess == status))

break; /* failed */

NSLog(@"stutas:%d",(int)status);

NSLog(@"Result: %d", result);



BOOL allowConnect = (result == kSecTrustResultUnspecified) || (result == kSecTrustResultProceed);

if (allowConnect) {

NSLog(@"success");

}else {

NSLog(@"error");

}

/* https://developer.apple.com/library/ios/technotes/tn2232/_index.html */

/* https://developer.apple.com/library/mac/qa/qa1360/_index.html */

/* kSecTrustResultUnspecified and kSecTrustResultProceed are success */

if(! allowConnect)

{

break; /* failed */

}



#if 0

/* Treat kSecTrustResultConfirm and kSecTrustResultRecoverableTrustFailure as success */

/* since the user will likely tap-through to see the dancing bunnies */

if(result == kSecTrustResultDeny || result == kSecTrustResultFatalTrustFailure || result == kSecTrustResultOtherError)

break; /* failed to trust cert (good in this case) */

#endif



// The only good exit point

return [[challenge sender] useCredential: [NSURLCredential credentialForTrust: serverTrust]

forAuthenticationChallenge: challenge];



} while(0);

}



// Bad dog

return [[challenge sender] cancelAuthenticationChallenge: challenge];



}

这里的关键在于result参数的值,根据官方文档的说明,判断result == kSecTrustResultUnspecified) || (result == kSecTrustResultProceed的值,若为ture,则该网站的CA被app信任成功,可以建立数据连接,这意味着所有由该CA签发的各个服务器证书都被信任,而访问其它没有被信任的任何网站都会连接失败。该CA文件既可以是SLL也可以是自签名。

NSURLConnection的其它代理方法

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
#pragma mark -- connect的异步代理方法

-(void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {

NSLog(@"请求被响应");

_mData = [[NSMutableData alloc]init];

}



-(void)connection:(NSURLConnection *)connection didReceiveData:(nonnull NSData *)data {

NSLog(@"开始返回数据片段");



[_mData appendData:data];

}



-(void)connectionDidFinishLoading:(NSURLConnection *)connection {

NSLog(@"链接完成");

//可以在此解析数据

NSString *receiveInfo = [NSJSONSerialization JSONObjectWithData:self.mData options:NSJSONReadingAllowFragments error:nil];

NSLog(@"received data:\\\\n%@",self.mData);

NSLog(@"received info:\\\\n%@",receiveInfo);

}



//链接出错

-(void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {

NSLog(@"error - %@",error);

}

总结与Demo

至此,HTTPS信任证书的问题得以解决,这不仅是为了响应Apple强制性使用ATS的要求,也是为了实际生产环境安全性的考虑,HTTPS是未来的趋势,建议尽早支持。

如需参考Demo请移步本人在Github上的开源项目