美文网首页
自签名证书适配https

自签名证书适配https

作者: 海耐射手 | 来源:发表于2021-04-16 14:42 被阅读0次

    最近项目中,需要使用自签名的 HTTPS 证书实现双向认证。网上的资料很多,但是存在各种各样的问题,与 iOS 版本、ATS 配置 等多方面因素有关。弄好之后先整一份记下来。完整的内容涉及到的内容比较多,还是要全面查阅文档,本文只记录最终的结果,和部分遇到的问题。
      本文中的代码,在 iOS 8.x 和 iOS 9.x 的模拟器中测试通过,iOS 10.x 模拟器和真机中测试通过。

    一、背景知识

    对于 HTTPS 认证,不管是单向还是双向,在客户端连接到服务端时,会触发客户端的 Authroization Challenge(没找到太合适的翻译,暂且理解为授权质询)回调,在处理 Authroization Challenge 之后,得到两个值:(不知道怎么翻译,随便写下)

    • NSURLSessionAuthChallengeDisposition 处置方式
      • NSURLSessionAuthChallengeUseCredential
        使用指定的凭证,凭证可能为空
      • NSURLSessionAuthChallengePerformDefaultHandling
        忽略凭证,使用默认的质询处理器
      • NSURLSessionAuthChallengeCancelAuthenticationChallenge
        整个请求将被取消; 凭证参数被忽略。
      • NSURLSessionAuthChallengeRejectProtectionSpace
        这个挑战被拒绝,并且应该尝试下一个 Authentication Protection Space;凭证参数被忽略。
    • NSURLCredential * 凭证
      可以根据回调时传入的信息,自己调用相关 API 获取凭证,也可以自己伪造

    将得到的两个值,作为回调函数的结果回传给系统,以完成 Authroization Challenge。

    以上过程仅是对 iOS 认证过程的分析,不过个人认为,网络模型是一致的,在不同技术中即便在实现细节上有所差异,但总体思路还是大同小异的。

    二、服务端认证

    对于采用通过 CA 购买的正式证书,只要没有特别要求,手机端不需要对 Authroization Challenge 做任何处理,就可以直接连接。
      如果是自签名证书,就需要做一些处理工作。iOS 8 及其之前的版本比较简单,而且目前 iOS 8 在市面上的保有量已经很少,不做细致讨论。iOS 9+ 之后引入了 ATS,带来的问题比较多所以从代码到配置上都要做相应调整。

    1、白名单方式

    步骤一(修改配置 Info.plist):
    (1)针对域名配置

    修改 Info.plist,将要访问的域名配置为 NSExceptionAllowsInsecureHTTPLoads,允许不安全的 HTTP 访问:

    <?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
    <plist version="1.0">
    <dict>
        <key>NSAppTransportSecurity</key>
        <dict>
            <key>NSExceptionDomains</key>
            <dict>
                <key>yourdomain.com</key>
                <dict>
                    <key>NSIncludesSubdomains</key>
                    <true/>
                    <key>NSExceptionAllowsInsecureHTTPLoads</key>
                    <true/>
                </dict>
            </dict>
        </dict>
    </dict>
    </plist>
    

    这里对于域名的配置,类似是个白名单的方式,在白名单中的域名,配置为允许不安全的 HTTPS 连接。不安全的证书原因很多,常见的可能是如下原因导致的:

    • 其根证书不被操作系统信任,如:自签证书
    • 证书过期或被吊销
    • 证书域名与实际域名不匹配

    配置中的 NSIncludesSubdomains 部分,建议把域名写为顶级域名,然后把 NSIncludesSubdomains 置 为 true包含子域名
      如果是特定的完整域名,如:www.yourdomain.com 则把 NSIncludesSubdomains 置 为 false。后面的说法,几次验证的效果不同。
    如果没有特别要求,建议使用第一种做法,写一级域名,然后包含其子域。

    (2)最简单粗暴的方法
    <?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
    <plist version="1.0">
    <dict>
        <key>NSAppTransportSecurity</key>
        <dict>
            <key>NSAllowsArbitraryLoads</key>
            <true/>
        </dict>
    </dict>
    </plist>
    

    这种方式:

    • 允许非安全的 HTTP 请求,iOS 9+ 默认是不允许 HTTP 连接的
    • 对于所有域名都可以使用自签名证书,不再需要逐个指定域名
    步骤二(修改代码):

    修改代码:

    // 安全策略
    // 同浏览器行为,以操作系统规则对服务器证书
    AFSecurityPolicy *policy = [AFSecurityPolicy policyWithPinningMode:AFSSLPinningModeNone];
    // 不校验域名,如果需要校验域名,需要采用内置证书的方式
    policy.validatesDomainName = NO;
    // 允许无效证书
    policy.allowInvalidCertificates = YES;
    
    // 为 SessionManager 配置安全策略
    AFHTTPSessionManager *mgr = [AFHTTPSessionManager manager];
    mgr.securityPolicy = policy;
    
    // 重要!!!设置缓存策略,避免缓存
    // AFNetworking 的 GET 方法缓存非常明显,一旦成功一次,后面就会直接使用缓存的结果,即便网络访问失败,也能返回成功数据,会对判断造成误导,所以一定要加上这一句!!!
    [mgr.requestSerializer setValue:@"no-cache" forHTTPHeaderField:@"Cache-Control"];
    // 发送请求
    [mgr GET:...];
    

    重要:
      白名单方式最简单,但是这样做只建立安全连接,但不会对服务端证书做校验,比如:不会校验证书与域名的一致性。这样做的问题是无法防御“中间人攻击”

    2、内置证书方式

    (1)基本实现

    白名单的方案不够安全,更为安全的做法:采用内置证书的方式,将用于校验的证书内置在客户端,不信任除此之外的证书。内置的证书,可以是服务端证书,或者是用于颁发服务端证书的 CA 的证书。具体要看证书具体的签发方式。
      内置的方式,是将证书转为 DER 格式,然后以 .cer 为扩展名,作为资源放到工程中,AFNetworking 就可以自动识别了。
      同时,代码要做如下调整:

    // 安全策略
    // 改为 AFSSLPinningModeCertificate
    AFSecurityPolicy *policy = [AFSecurityPolicy policyWithPinningMode:AFSSLPinningModeCertificate];
    // 指定验证域名。如果访问的域名与证书域名不一致,则不能通过
    // 如果需要做域名校验,必须使用 Pinned 方式。白名单方式,不集成证书,无法校验域名
    policy.validatesDomainName = YES;
    // 对于自签证书,使用这个选项
    policy.allowInvalidCertificates = YES;
    // cerData1、cerData2 为 NSData,内容为 DER 格式证书
    // 证书可以是 CA 证书,也可以是服务端部署的证书,这一步可选,AFN 可以自动识别
    policy.pinnedCertificates = [NSSet setWithObjects:cerData1, cerData2, nil];
    
    // 为 SessionManager 配置安全策略
    AFHTTPSessionManager *mgr = [AFHTTPSessionManager manager];
    mgr.securityPolicy = policy;
    
    [mgr.requestSerializer setValue:@"no-cache" forHTTPHeaderField:@"Cache-Control"];
    // 发送请求
    [mgr GET:...];
    
    ☆☆☆ 默认校验规则

    上面说的两种方式,实际上都是使用了 AFNetworking 的默认校验规则,并且根据默认规则做了个简单实现。其规则是这样的:
    AFNetworking 的 AFSecurityPolicy 类有如下方法:

    - (BOOL)evaluateServerTrust:(SecTrustRef)serverTrust forDomain:(NSString *)domain
    

    涉及到三个因素:

    • securityPolicy.allowInvalidCertificates 是否允许无效证书,个人理解这里所说的无效证书,是指类似浏览器校验行为,操作系统不认的证书
    • securityPolicy.validatesDomainName 校验域名
    • securityPolicy.SSLPinningMode PinningMode

    表面看来有如下规律:

    • 如果要使用自签名证书,必须指定 allowInvalidCertificates = YES;,否则不能通过;
    • 如果 allowInvalidCertificates == YES 并且 SSLPinningMode == AFSSLPinningModeNone,就是说只校验服务端证书,不管客户端内置证书,并指定为允许无效证书,则可以通过;
    • SSLPinningMode == AFSSLPinningModeCertificate 或 AFSSLPinningModePublicKey 则看本地内置证书与服务端证书是否匹配
    • 使用了 AFSSLPinningModeCertificateAFSSLPinningModePublicKey,会导致客户端没有内置证书的网站都不能访问,如:https://www.baidu.com

    默认校验规则总结:
    先约定个几个名词:

    正规证书 <=> 操作系统认可 and (域名一致 or 不校验域名)
    有效证书 <=> 正规证书 or 允许非正规证书
    无效证书 <=> 操作系统不认可 and 不允许非正规证书

    • 先检查 SSLPinningMode,如果为 AFSSLPinningModeNone,检查证书是否为有效证书即为校验结果;
    • 如果 SSLPinningModeAFSSLPinningModeCertificateAFSSLPinningModePublicKey,检查证书,如果为无效证书,校验结果不通过;如果为有效证书,后续则根据本地集成证书与服务端证书一起校验结果,作为最终校验结果。
    • validatesDomainName,只是判定因素之一,虽然影响整体校验结果,但不影响校验逻辑。如:虽然证书合法,指定做域名校验,但是证书域名与访问域名不一致,结果是不通过。

    这部分的校验,可以参见官方文档:Overriding TLS Chain Validation Correctly

    (2)个性化处理:指定身份验证质询回调块

    对于“标准场景”,达到可访问的目的,没有额外要求,上述代码已经可以了。但是对于需要额外处理的场景,如:失败的时候给出对应提示,需要使用如下方法,来指定用于处理授权质询的回调块:

    // AFURLSessionManager 类
    // 指定用于处理 身份验证质询 的回调块
    – setSessionDidReceiveAuthenticationChallengeBlock:
    

    这部分的实现详情可以参见 AFNetworking 中 AFURLSessionManager.m 文件里如下方法:

    // AFURLSessionManager.m
    // 处理身份验证质询
    - URLSession:didReceiveChallenge:completionHandler:
    

    在这个方法中,会先查看用户是否指定了自己的回调块,如果指定了就执行用户自己的回调块,否则执行默认实现。编写自己的回调方法时,可以参考默认实现。

    注意:默认实现中,只实现了服务端验证。对于客户端验证部分,只做了如下处理:

    *credential = nil;
    disposition = NSURLSessionAuthChallengePerformDefaultHandling;
    

    如果要做客户端认证,重写这部分代码即可,后文中会提到。

    调试注意事项:

    测试时有一点需要注意,如果使用 GET 方法,应保证每次都真实发送了请求,而不是使用缓存,避免影响测试效果。坑啊!

    • (推荐)客户端处理:Request 的 Header 中,指定 Cache-Controlno-cache
    • 处理 URL:为 URL 增加时间戳
    • 服务端处理:Response 的 Header 中,指定 Cache-Controlno-cache
    • iOS 端设置:指定缓存策略(不推荐)

    3、UIWebView

    (1)使用 AFNetworking

    AFNetworking 提供了 UIWebView+AFNetworking Category,可以通过这个分类为 UIWebView 指定 sessionManager,并调用新增加的 - loadRequest:MIMEType:textEncodingName:progress:success:failure: 方法来进行加载。但是在 Cordova 这样的组件中,还会使用 UIWebView 默认的 - loadRequest: 方法,可以配合 Method Swizzling 解决该问题。不过这样的话还是有个问题,会导致 UIWebView 的历史丢失,无法执行“返回”操作,原因是没有使用 UIWebView 自己的方法去访问。

    (2)使用 NSURLProtocol

    目前对于网络认证相关的处理,效果最好、侵入性最小、对已有代码逻辑影响最小的,是 NSURLProtocol 方式。这里有个用于使 UIWebView 支持客户端认证的插件,对于服务端认证一样有效,参见 https://github.com/mwaylabs/cordova-plugin-client-certificate
      题外话,NSURLProtocol 对于很多特定场景来说更为有效。比如:曾经有项目使用了 HTTP Basic Authorization 认证。如果不使用 NSURLProtocol 的方案,可能会导致以下两种情况不能通过认证:

    • 302 引发的跳转不能自动带上认证信息
    • Web 页面上的 <img><script> 标签、CSS、Ajax 请求不能通过认证

    三、客户端认证

    对于双向 HTTPS 认证来说,服务端认证是基础。客户端认证的前提,是先实现服务端认证,然后在此基础上做一下补充。
      在前文中服务端授权质询处理相关的描述中,在对应位置写客户端认证的代码即可。客户端需要集成 PKCS12 格式的证书文件(由证书及其私钥文件合成),代码中内置对应密码。
      详见代码示例 iOSSSLDemo

    四、相关因素及讨论

    1、证书加载

    如果使用 AFNetworking 的话,加载证书非常简单,只要把格式为 DER 的证书(扩展名一般为 .cer.der)集成到 Bundle,然后通过以下代码来自动加载:

    policy.pinnedCertificates = [AFSecurityPolicy certificatesInBundle:[NSBundle mainBundle]];
    
    

    如果需要在运行时动态加载临时获取的证书,可以通过

    policy.pinnedCertificates = [NSSet setWithObjects:cerData1, cerData2, nil];
    
    

    来实现。其内容为证书的 NSData 组成的 NSSet

    2、证书的校验

    @interface NSURLRequest (SSL)
    @end
    
    @implementation NSURLRequest (SSL)
    
    + (BOOL)allowsAnyHTTPSCertificateForHost:(NSString *)host {
        return YES;
    }
    @end
    
    

    这种使用 Category 的写法,也导致不会对证书进行校验。不过此方法有两个问题:

    1. 不校验证书,导致安全级别降低,容易被“中间人”方式攻击;
    2. 此方法为私有方法,不建议使用。

    3、iOS 8

    在 iOS 8 中,如果使用 AFNetworking 来实现自签名证书的认证,非常简单,只要代码部分按结论中的描述来编写即可。
      有一点不太确定的,网上的资料说必须加载证书,但是实际测试,不加也可以,这个可能跟 AFSSLPinningMode 有关系,不过由于目前 iOS 8 保有量很少了,所以不再深入了。

    4、iOS 9+

    对于 iOS 9+ 的情况,苹果加入了 ATS,所以必须做 iOS 9 的适配 按照结论中说的,修改 ATS 部分的设置。

    5、AFSSLPinningMode

    AFSSLPinningMode 是安全策略的模式指定。

    #import <Security/Security.h>
    
    typedef NS_ENUM(NSUInteger, AFSSLPinningMode) {
        AFSSLPinningModeNone,    
        //表示不做SSL pinning,只跟浏览器一样在系统的信任机构列表里验证服务端返回的证书。若证书是信任机构签发的就会通过,若是自己服务器生成的证书,这里是不会通过的。
    
        AFSSLPinningModeCertificate,
        //表示用证书绑定方式验证证书,需要客户端保存有服务端的证书拷贝,这里验证分两步,第一步验证证书的域名/有效期等信息,第二步是对比服务端返回的证书跟客户端返回的是否一致。
    
        AFSSLPinningModePublicKey,
        //这个模式同样是用证书绑定方式验证,客户端要有服务端的证书拷贝,只是验证时只验证证书里的公钥,不验证证书的有效期等信息。
    
    };
    

    相关文章

      网友评论

          本文标题:自签名证书适配https

          本文链接:https://www.haomeiwen.com/subject/ixzsbctx.html