这篇文章是我一边学习证书验证一边记录的内容,
稍微整理了下,共扯了三部份内容:html
HTTPS 是运行在 TLS/SSL 之上的 HTTP,与普通的 HTTP 相比,在数据传输的安全性上有很大的提高。
要了解它安全性的巧妙之处,须要先简单地了解对称加密和非对称加密的区别:ios
为了提升安全性,咱们经常使用的作法是使用对称加密的手段加密数据。但是只使用对称加密的话,双方通讯的开始总会以明文的方式传输密钥。那么从一开始这个密钥就泄露了,谈不上什么安全。因此 TLS/SSL 在握手的阶段,结合非对称加密的手段,保证只有通讯双方才知道对称加密的密钥。大概的流程以下:web
因此,HTTPS 实现传输安全的关键是:在 TLS/SSL 握手阶段保证仅有通讯双方获得 Session Key!算法
X.509 应该是比较流行的 SSL 数字证书标准,包含(但不限于)如下的字段:后端
字段 | 值说明 |
---|---|
对象名称(Subject Name) | 用于识别该数字证书的信息 |
共有名称(Common Name) | 对于客户证书,一般是相应的域名 |
证书颁发者(Issuer Name) | 发布并签署该证书的实体的信息 |
签名算法(Signature Algorithm) | 签名所使用的算法 |
序列号(Serial Number) | 数字证书机构(Certificate Authority, CA)给证书的惟一整数,一个数字证书一个序列号 |
生效期(Not Valid Before) | (`・ω・´) |
失效期(Not Valid After) | (╯°口°)╯(┴—┴ |
公钥(Public Key) | 可公开的密钥 |
签名(Signature) | 经过签名算法计算证书内容后获得的数据,用于验证证书是否被篡改 |
除了上述所列的字段,还有不少拓展字段,在此不一一详述。浏览器
下图为 Wikipedia 的公钥证书:安全
数字证书的生成是分层级的,下一级的证书须要其上一级证书的私钥签名。
因此后者是前者的证书颁发者,也就是说上一级证书的 Subject Name 是其下一级证书的 Issuer Name。服务器
在获得证书申请者的一些必要信息(对象名称,公钥私钥)以后,证书颁发者经过 SHA-256 哈希获得证书内容的摘要,再用本身的私钥给这份摘要加密,获得数字签名。综合已有的信息,生成分别包含公钥和私钥的两个证书。session
扯到这里,就有几个问题:数据结构
问:若是说发布一个数字证书必需要有上一级证书的私钥加密,那么最顶端的证书——根证书怎么来的?
根证书是自签名的,即用本身的私钥签名,不须要其余证书的私钥来生成签名。
问:怎么验证证书是有没被篡改?
当客户端走 HTTPS 访问站点时,服务器会返回整个证书链。如下图的证书链为例:
要验证
*.wikipedia.org
这个证书有没被篡改,就要用到GlobalSign Organization Validation CA - SHA256 - G2
提供的公钥解密前者的签名获得摘要 Digest1,咱们的客户端也计算前者证书的内容获得摘要 Digest2。对比这两个摘要就能知道前者是否被篡改。后者同理,使用GlobalSign Root CA
提供的公钥验证。当验证到到受信任的根证书时,就能肯定*.wikipedia.org
这个证书是可信的。
问:为何上面那个根证书 GlobalSign Root CA
是受信任的?
数字证书认证机构(Certificate Authority, CA)签署和管理的 CA 根证书,会被归入到你的浏览器和操做系统的可信证书列表中,并由这个列表判断根证书是否可信。因此不要随便导入奇奇怪怪的根证书到你的操做系统中。
问:生成的数字证书(如 *.wikipedia.org
)均可用来签署新的证书吗?
不必定。以下图,拓展字段里面有个叫 Basic Constraints 的数据结构,里面有个字段叫路径长度约束(Path Length Constraint),代表了该证书能继续签署 CA 子证书的深度,这里为0,说明这个
GlobalSign Organization Validation CA - SHA256 - G2
只能签署客户端证书,而客户端证书不能用于签署新的证书,CA 子证书才能这么作。
在 Overriding TLS Chain Validation Correctly 中提到:
When a TLS certificate is verified, the operating system verifies its chain of trust. If that chain of trust contains only valid certificates and ends at a known (trusted) anchor certificate, then the certificate is considered valid.
因此在 iOS 中,证书是否有效的标准是:
信任链中若是只含有有效证书而且以可信锚点(trusted anchor)结尾,那么这个证书就被认为是有效的。
其中可信锚点指的是系统隐式信任的证书,一般是包括在系统中的 CA 根证书。不过你也能够在验证证书链时,设置自定义的证书做为可信的锚点。
具体到使用 NSURLSession 走 HTTPS 访问网站,-URLSession:didReceiveChallenge:completionHandler:
回调中会收到一个 challenge,也就是质询,须要你提供认证信息才能完成链接。这时候能够经过 challenge.protectionSpace.authenticationMethod
取得保护空间要求咱们认证的方式,若是这个值是 NSURLAuthenticationMethodServerTrust
的话,咱们就能够插手 TLS 握手中“验证数字证书有效性”这一步。
系统的默认实现(也即代理不实现这个方法)是验证这个信任链,结果是有效的话则根据 serverTrust 建立 credential 用于同服务端确立 SSL 链接。不然会获得 “The certificate for this server is invalid...” 这样的错误而没法访问。
好比在访问 https://www.google.com 的时候咧,咱们不实现这个方法也能访问成功的。系统对 Google 服务器返回来的证书链,从叶节点证书往根证书层层验证(有效期、签名等等),遇到根证书时,发现做为可信锚点的它存在与可信证书列表中,那么验证就经过,容许与服务端创建链接。
而当咱们访问 https://www.12306.cn 时,就会出现 "The certificate for this server is invalid. You might be connecting to a server that is pretending to be “www.12306.cn” which could put your confidential information at risk." 的错误。缘由就是系统在验证到根证书时,发现它是自签名、不可信的。
若是咱们要实现这个代理方法的话,须要提供 NSURLSessionAuthChallengeDisposition(处置方式)和 NSURLCredential(资格认证)这两个参数给 completionHandler 这个 block:
1 -(void)URLSession:(NSURLSession *)session 2 didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge 3 completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition, 4 NSURLCredential * _Nullable))completionHandler { 5 6 // 若是使用默认的处置方式,那么 credential 就会被忽略 7 NSURLSessionAuthChallengeDisposition disposition = NSURLSessionAuthChallengePerformDefaultHandling; 8 NSURLCredential *credential = nil; 9 10 if ([challenge.protectionSpace.authenticationMethod 11 isEqualToString: 12 NSURLAuthenticationMethodServerTrust]) { 13 14 /* 调用自定义的验证过程 */ 15 if ([self myCustomValidation:challenge]) { 16 credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust]; 17 if (credential) { 18 disposition = NSURLSessionAuthChallengeUseCredential; 19 } 20 } else { 21 /* 无效的话,取消 */ 22 disposition = NSURLSessionAuthChallengeCancelAuthenticationChallenge 23 } 24 } 25 if (completionHandler) { 26 completionHandler(disposition, credential); 27 } 28 }
在 [self myCustomValidation:challenge]
调用自定义验证过程,结果是有效的话才建立 credential 确立链接。
自定义的验证过程,须要先拿出一个 SecTrustRef 对象,它是一种执行信任链验证的抽象实体,包含着验证策略(SecPolicyRef)以及一系列受信任的锚点证书,而咱们能作的也是修改这两样东西而已。
1 SecTrustRef trust = challenge.protectionSpace.serverTrust;
拿到 trust 对象以后,能够用下面这个函数对它进行验证。
1 static BOOL serverTrustIsVaild(SecTrustRef trust) { 2 BOOL allowConnection = NO; 3 4 // 假设验证结果是无效的 5 SecTrustResultType trustResult = kSecTrustResultInvalid; 6 7 // 函数的内部递归地从叶节点证书到根证书的验证 8 OSStatus statue = SecTrustEvaluate(trust, &trustResult); 9 10 if (statue == noErr) { 11 // kSecTrustResultUnspecified: 系统隐式地信任这个证书 12 // kSecTrustResultProceed: 用户加入本身的信任锚点,显式地告诉系统这个证书是值得信任的 13 14 allowConnection = (trustResult == kSecTrustResultProceed 15 || trustResult == kSecTrustResultUnspecified); 16 } 17 return allowConnection; 18 }
这个函数何时调用彻底取决于你的需求,若是你不想对验证策略作修改而直接调用的话,那你竟然还看到这里!?(╯‵□′)╯︵┻━┻
能够经过如下的代码得到当前的验证策略:
1 CFArrayRef policiesRef;
2 SecTrustCopyPolicies(trust, &policiesRef);
打印 policiesRef 后,你会发现默认的验证策略就包含了域名验证,即“服务器证书上的域名和请求域名是否匹配”。若是你的一个证书须要用来链接不一样域名的主机,或者你直接用 IP 地址去链接,那么你能够重设验证策略以忽略域名验证:
1 NSMutableArray *policies = [NSMutableArray array]; 2 3 // BasicX509 不验证域名是否相同 4 SecPolicyRef policy = SecPolicyCreateBasicX509(); 5 [policies addObject:(__bridge_transfer id)policy]; 6 SecTrustSetPolicies(trust, (__bridge CFArrayRef)policies); 7 8
后再调用 serverTrustIsVaild()
验证。
可是若是不验证域名的话,安全性就会大打折扣。拿浏览器举🌰:
试想你要传输报文到 https://www.real-website.com ,然而因为域名劫持,把你带到了 https://www.real-website.cn 这个🎣网站,大概有如下两种结果:
你可能会问:公钥证书是每一个人都能获得的,钓鱼网站能不能返回真正的公钥证书给咱们呢?
我以为是能够的,然而这并无什么卵用。没有私钥的钓鱼服务器没法得到第三个随机数,没法生成 Session Key,也就不能对咱们传给它的数据进行解密了。
在 App 中想要防止上面提到的中间人公鸡攻击,比较好的作法是将公钥证书打包进 App 中,而后在收到服务端证书链的时候,可以有效地验证服务端是否可信,这也是验证自签名的证书链所必须作的。
假设你的服务器返回:[你的自签名的根证书] -- [你的二级证书] -- [你的客户端证书],系统是不信任这个三个证书的。
因此你在验证的时候须要将这三个的其中一个设置为锚点证书,固然,多个也行。
好比将 [你的二级证书] 做为锚点后,SecTrustEvaluate()
函数只要验证到 [你的客户端证书] 确实是由 [你的二级证书] 签署的,那么验证结果为 kSecTrustResultUnspecified
,代表了 [你的客户端证书] 是可信的。下面是设置锚点证书的作法:
1 NSMutableArray *certificates = [NSMutableArray array]; 2 3 NSDate *cerData = /* 在 App Bundle 中你用来作锚点的证书数据,证书是 CER 编码的,常见扩展名有:cer, crt...*/ 4 5 SecCertificateRef cerRef = SecCertificateCreateWithData(NULL, (__bridge CFDataRef)cerData); 6 7 [certificates addObject:(__bridge_transfer id)cerRef]; 8 9 // 设置锚点证书。 10 SecTrustSetAnchorCertificates(trust, (__bridge CFArrayRef)certificates);
只调用 SecTrustSetAnchorCertificates ()
这个函数的话,那么就只有做为参数被传入的证书做为锚点证书,连系统自己信任的 CA 证书不能做为锚点验证证书链。要想恢复系统中 CA 证书做为锚点的功能,还要再调用下面这个函数:
1 // true 表明仅被传入的证书做为锚点,false 容许系统 CA 证书也做为锚点
2 SecTrustSetAnchorCertificatesOnly(trust, false);
这样,再调用 serverTrustIsVaild()
验证证书有效性就能成功了。
上面说的是没通过 CA 认证的自签证书的验证,而 CA 的证书链的验证方式也是同样,不一样点在不可信锚点的证书类型不同而已:前者的锚点是自签的须要被打包进 App 用于验证,后者的锚点可能原本就存在系统之中了。不过我脑补了这么的一个坑:
假如咱们使用的是 CA 根证书签署的数字证书,并且只用这个 CA 根证书做为锚点,在不验证域名的状况下,是否是就会在握手阶段信任被同一个 CA 根证书签名的伪造证书呢?
Overriding TLS Chain Validation Correctly
上文有什么我理解得不正确、或表达不许确的地方,烦请指教。🌝
公司的接口通常会两种协议的,一种HTTP,一种HTTPS的,HTTP 只要请求,服务器就会响应,若是咱们不对请求和响应作出加密处理,全部信息都是会被检测劫持到的,是很不安全的,客户端加密可使用我这套工具类进行处理:文章地址
可是不论在任什么时候候,都应该将服务置于HTTPS上,由于它能够避免中间人攻击的问题,还自带了基于非对称密钥的加密通道!现实是这些年涌现了大量速成的移动端开发人员,这些人每每基础不好,彻底不了解加解密为什么物,使用HTTPS后,能够省去教育他们各类加解密技术,生活轻松多了。
简答说,HTTPS 就是 HTTP协议加了一层SSL协议的加密处理,SSL 证书就是遵照 SSL协议,由受信任的数字证书颁发机构CA(如GlobalSign,wosign),在验证服务器身份后颁发,这是须要花钱滴,签发后的证书做为公钥通常放在服务器的根目录下,便于客户端请求返回给客户端,私钥在服务器的内部中心保存,用于解密公钥。
一、客户端发送请求,服务器返回公钥给客户端;
二、客户端生成对称加密秘钥,用公钥对其进行加密后,返回给服务器;
三、服务器收到后,利用私钥解开获得对称加密秘钥,保存;
四、以后的交互都使用对称加密后的数据进行交互。
谈下证书
简单说,证书有两种,一种是正经的:![]()
CA颁发的证书
一种是不正经的:![]()
本身生成签发的证书
若是遇到正经的证书,咱们直接用AFNetworking 直接请求就行了,AFNetworking 内部帮咱们封装了HTTPS的请求方式,可是大部分公司接口都是不正经的证书,这时须要咱们作如下几步:
一、将服务器的公钥证书拖到Xcode中
二、修改验证模式
manager.securityPolicy = [AFSecurityPolicy policyWithPinningMode:AFSSLPinningModePublicKey];
原理:
简单来讲,就是你本能够修改AFN这个设置来容许客户端接收服务器的任何证书,可是这么作有个问题,就是你没法验证证书是不是你的服务器后端的证书,给中间人攻击,即经过重定向路由来分析伪造你的服务器端打开了大门。
AFSecurityPolicy *securityPolicy = [AFSecurityPolicy defaultPolicy];
securityPolicy.allowInvalidCertificates = YES;
解决方法:AFNetworking是容许内嵌证书的,经过内嵌证书,AFNetworking就经过比对服务器端证书、内嵌的证书、站点域名是否一致来验证链接的服务器是否正确。因为CA证书验证是经过站点域名进行验证的,若是你的服务器后端有绑定的域名,这是最方便的。将你的服务器端证书,若是是pem格式的,用下面的命令转成cer格式
openssl x509 -in <你的服务器证书>.pem -outform der -out server.cer
而后将生成的server.cer文件,若是有自建ca,再加上ca的cer格式证书,引入到app的bundle里,AFNetworking在
AFSecurityPolicy *securityPolicy = [AFSecurityPolicy AFSSLPinningModeCertificate];
或者
AFSecurityPolicy *securityPolicy = [AFSecurityPolicy AFSSLPinningModePublicKey];
状况下,会自动扫描bundle中.cer的文件,并引入,这样就能够经过自签证书来验证服务器惟一性了。
AFSecurityPolicy分三种验证模式:
AFSSLPinningModeNone
这个模式表示不作SSL pinning,
只跟浏览器同样在系统的信任机构列表里验证服务端返回的证书。若证书是信任机构签发的就会经过,如果本身服务器生成的证书就不会经过。AFSSLPinningModeCertificate
这个模式表示用证书绑定方式验证证书,须要客户端保存有服务端的证书拷贝,这里验证分两步,第一步验证证书的域名有效期等信息,第二步是对比服务端返回的证书跟客户端返回的是否一致。
AFSSLPinningModePublicKey
这个模式一样是用证书绑定方式验证,客户端要有服务端的证书拷贝,
只是验证时只验证证书里的公钥,不验证证书的有效期等信息。只要公钥是正确的,就能保证通讯不会被窃听,由于中间人没有私钥,没法解开经过公钥加密的数据。