Https浅析与实现(iOS)

前言

随着 iOS9 的发布以及对 iOS 平台的安全问题的关注度的提高,iOS 安全问题慢慢被提上日程。最近了解了一下 iOS9 下 Https 的相关实现,和大家分享一下。

Https 浅析

Https 和 http 有何异同

https 和 http 从数据解析和交换来说本质上没有任何区别,https 可以看做经过校验和加密的 http 通信。也可以说在网络应用层,http+ssl/tsl 形成了 https。

Https 的通信过程

  1. 客户端产生一个随机数,将这个随机数以及客户端支持的加密算法发送给服务器.
  2. 服务器接到请求,确认双方使用的加密算法并产生第二个随机数,把证书和第二个随机数发送给客户端
  3. 客户端接收到证书之后验证服务器的身份。无效,则取消连接。有效,则生成第三个随机数,领用证书中的公钥对第三个随机数进行加密,传输给服务器。
  4. 服务器接收到第三个随机数后利用服务器上的私钥进行解密,然后得到第三个随机数
  5. 客户端和服务器使用这个三个随机数生成的“对话秘钥”进行加密,进行后续的通信过程。

安全性的保证:

虽然第一个和第二个随机数是明文,但是第三个随机数使用非对称加密方法进行加密,而且私钥从来没有在网络中进行传输,从理论上讲整个通信过程是安全的。

相关概念解释

SSL/TLS 看这里:

SSL/TLS 协议运行机制的概述

图解 SSL/TLS 协议

SSL/TLS 版本相关

数字证书:
该证书包含了公钥等信息,一般是由服务器发给客户端,接收方通过验证这个证书是不是由信赖的 CA 签发,或者与本地的证书相对比,来判断证书是否可信;假如需要双向验证,则服务器和客户端都需要发送数字证书给对方验证;

数字证书是一个电子文档,其中包含了持有者的信息、公钥以及证明该证书有效的数字签名。而数字证书以及相关的公钥管理和验证等技术组成了 PKI(公钥基础设施)规范体系。一般来说,数字证书是由数字证书认证机构(Certificate authority,即 CA)来负责签发和管理,并承担 PKI 体系中公钥合法性的检验责任;数字证书的类型有很多,而 HTTPS 使用的是 SSL 证书。

怎么来验证数字证书是由 CA 签发的,而不是第三方伪造的呢? 在回答这个问题前,我们需要先了解 CA 的组织结构。首先,CA 组织结构中,最顶层的就是根 CA,根 CA 下可以授权给多个二级 CA,而二级 CA 又可以授权多个三级 CA,所以 CA 的组织结构是一个树结构。对于 SSL 证书市场来说,主要被 Symantec(旗下有 VeriSign 和 GeoTrust)、Comodo SSL、Go Daddy 和 GlobalSign 瓜分。 了解了 CA 的组织结构后,来看看数字证书的签发流程。

数字证书的签发机构 CA,在接收到申请者的资料后进行核对并确定信息的真实有效,然后就会制作一份符合 X.509 标准的文件。证书中的证书内容包括了持有者信息和公钥等都是由申请者提供的,而数字签名则是 CA 机构对证书内容进行 hash 加密后等到的,而这个数字签名就是我们验证证书是否是有可信 CA 签发的数据。

接收端接到一份数字证书 Cer1 后,对证书的内容做 Hash 等到 H1;然后在签发该证书的机构 CA1 的数字证书中找到公钥,对证书上数字签名进行解密,得到证书 Cer1 签名的 Hash 摘要 H2;对比 H1 和 H2,假如相等,则表示证书没有被篡改。但这个时候还是不知道 CA 是否是合法的,我们看到上图中有 CA 机构的数字证书,这个证书是公开的,所有人都可以获取到。而这个证书中的数字签名是上一级生成的,所以可以这样一直递归验证下去,直到根 CA。根 CA 是自验证的,即他的数字签名是由自己的私钥来生成的。合法的根 CA 会被浏览器和操作系统加入到权威信任 CA 列表中,这样就完成了最终的验证。所以,一定要保护好自己环境(浏览器/操作系统)中根 CA 信任列表,信任了根 CA 就表示信任所有根 CA 下所有子级 CA 所签发的证书,不要随便添加根 CA 证书。

Https 实现(iOS)

证书准备

  1. 创建私钥:

    openssl genrsa -out root/root-key.pem 1024

  2. 创建证书请求:

    openssl req -new -out root/root-req.csr -key root/root-key.pem

  3. 自签署证书:

    openssl x509 -req -in root/root-req.csr -out root/root-cert.pem -signkey root/root-key.pem -days 3650

  4. 将证书导出成浏览器支持的.p12 格式:

    openssl pkcs12 -export -clcerts -in root/root-cert.pem -inkey root/root-key.pem -out root/root.p12

因为我们只需要验证一次证书,我们只需要创建一次就好了,不需要像原文那样创建那么多。我们客户端需呀的 cer 文件也可以由 pem 文件直接转换导出即可。

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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
系统默认验证流程
// Now start the connection
NSURL \*httpsURL = [NSURL URLWithString:@"https://www.google.com"];
self.connection = [NSURLConnection connectionWithRequest:[NSURLRequest requestWithURL:httpsURL] delegate:self];

//回调

- (void)connection:(NSURLConnection _)connection willSendRequestForAuthenticationChallenge:(NSURLAuthenticationChallenge _)challenge {

//1)获取trust object
SecTrustRef trust = challenge.protectionSpace.serverTrust;
SecTrustResultType result;

//2)SecTrustEvaluate对trust进行验证
OSStatus status = SecTrustEvaluate(trust, &result);
if (status == errSecSuccess && (result == kSecTrustResultProceed || result == kSecTrustResultUnspecified)) {

//3)验证成功,生成NSURLCredential凭证cred,告知challenge的sender使用这个凭证来继续连接
NSURLCredential *cred = [NSURLCredential credentialForTrust:trust];
[challenge.sender useCredential:cred forAuthenticationChallenge:challenge];

} else {

//5)验证失败,取消这次验证流程
[challenge.sender cancelAuthenticationChallenge:challenge];

}

}

自建证书验证流程
//先导入证书
NSString _ cerPath = ...; //证书的路径
NSData _ cerData = [NSData dataWithContentsOfFile:cerPath];
SecCertificateRef certificate = SecCertificateCreateWithData(NULL, (\_\_bridge CFDataRef)(cerData));
self.trustedCertificates = @[CFBridgingRelease(certificate)];

//回调

- (void)connection:(NSURLConnection _)connection willSendRequestForAuthenticationChallenge:(NSURLAuthenticationChallenge _)challenge {

//1)获取trust object
SecTrustRef trust = challenge.protectionSpace.serverTrust;
SecTrustResultType result;
//注意:这里将之前导入的证书设置成下面验证的Trust Object的anchor certificate
SecTrustSetAnchorCertificates(trust, (__bridge CFArrayRef)self.trustedCertificates);

//2)SecTrustEvaluate会查找前面SecTrustSetAnchorCertificates设置的证书或者系统默认提供的证书,对trust进行验证
OSStatus status = SecTrustEvaluate(trust, &result);
if (status == errSecSuccess && (result == kSecTrustResultProceed || result == kSecTrustResultUnspecified)) {

//3)验证成功,生成NSURLCredential凭证cred,告知challenge的sender使用这个凭证来继续连接
NSURLCredential *cred = [NSURLCredential credentialForTrust:trust];
[challenge.sender useCredential:cred forAuthenticationChallenge:challenge];

} else {

//5)验证失败,取消这次验证流程
[challenge.sender cancelAuthenticationChallenge:challenge];
}

}

AFNetworking 对 Https 的支持

因为 AFNetworking 直接会遍历 bundle 找到对应的 cer 证书文件,故不需要添加证书路径,只需要把证书添加到工程中即可。

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
NSURL _ url = [NSURL URLWithString:@"https://www.google.com"];
AFHTTPRequestOperationManager _ requestOperationManager = [[AFHTTPRequestOperationManager alloc] initWithBaseURL:url];
dispatch_queue_t requestQueue = dispatch_create_serial_queue_for_name("kRequestCompletionQueue");
requestOperationManager.completionQueue = requestQueue;

AFSecurityPolicy \* securityPolicy = [AFSecurityPolicy policyWithPinningMode:AFSSLPinningModeCertificate];

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

//validatesDomainName 是否需要验证域名,默认为 YES;
//假如证书的域名与你请求的域名不一致,需把该项设置为 NO
//主要用于这种情况:客户端请求的是子域名,而证书上的是另外一个域名。因为 SSL 证书上的域名是独立的,假如证书上注册的域名是www.google.com,那么mail.google.com是无法验证通过的;当然,有钱可以注册通配符的域名*.google.com,但这个还是比较贵的。
securityPolicy.validatesDomainName = NO;

//validatesCertificateChain 是否验证整个证书链,默认为 YES
//设置为 YES,会将服务器返回的 Trust Object 上的证书链与本地导入的证书进行对比,这就意味着,假如你的证书链是这样的:
//GeoTrust Global CA
// Google Internet Authority G2
// _.google.com
//那么,除了导入_.google.com 之外,还需要导入证书链上所有的 CA 证书(GeoTrust Global CA, Google Internet Authority G2);
//如是自建证书的时候,可以设置为 YES,增强安全性;假如是信任的 CA 所签发的证书,则建议关闭该验证;
securityPolicy.validatesCertificateChain = NO;

requestOperationManager.securityPolicy = securityPolicy;

AFHTTPSessionManager 与之基本一致,不再详述。

参考博文:

AFNetworking 2.0 实现自签名 SSL 的 HTTPS 网络连接