证书链和TLS Pinning

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了证书链和TLS Pinning相关的知识,希望对你有一定的参考价值。

参考技术A 摘自  HTTPS 精读之 TLS 证书校验  

这篇讲证书校验,写得很好。

1. 浏览器对服务器发送了一次请求,包含协议版本号、一个客户端生成的随机数(Client random),以及客户端支持的加密方法。

2. 服务器确认双方使用的加密方法,并给出数字证书、以及一个服务器生成的随机数(Server random)。

3. 浏览器确认数字证书有效,然后生成一个新的随机数(Premaster secret),并使用数字证书中的公钥,加密这个随机数,发给服务器。

4. 服务器使用自己的私钥,获取浏览器发来的随机数(即Premaster secret)。

5. 服务器和浏览器根据约定的加密方法,使用前面的三个随机数,生成"对话密钥"(session key),用来加密接下来的整个对话过程。

X.509 除了规范证书的内容之外,还规范了如何获取 CRL 以及 Certificate Chain 的验证算法。X.509 规范由国际电信联盟(ITU)定义, RFC 5280  只是定义了 X.509 的用法。

文章最开始,我们访问  https://www.youzan.com  时,浏览器并非只拿到了一个证书,而是一个证书链:

证书「*. http://youzan.com 」的 Issuer 就是它的父节点「Go Daddy Secure Certificate Authority」。因为 UA(浏览器或操作系统)中会预先内置一些权威 CA 签发的根证书(Root Certificate)或中间证书(Intermediate Certificate),例如上面的 「Go Daddy Secure Certificate Authority」和 「Go Daddy Root Certificate Authority」。

                                                                                      Chain of trust - from wikipedia

当获得证书链之后,我们就可以很轻松的往上回溯到被 UA 信任的证书,虽然 UA 内置的可能是中间证书(Intermediate Certificate),但是如果一个 End-Entity 证书即使回溯到跟证书(Root Certificate)也没有在 UA 的受信列表中找到,那么这个站点就会被标记为不安全,例如 12306 的主页被标记为 “Not Secure",因为它的根证书不被信任。

我们上面所分析的校验方式属于单向校验,仅仅是客户端对服务端证书进行校验,这种方式无法避免中间人攻击(Man-In-the-Middle-Attack)。我们日常开发中用 Charles 抓包时, Charles 就扮演了一个中间人的角色 。抓包之前,我们需要在手机上安装一个 Charles 提供的根证书(Root Certificate),这个根证书加入到手机的 Trust Store 之后,它所签发的证书都会被 UA 认作可信。那么 Charles 就可以肆无忌惮地代表真正的 UA 与服务端建立连接,因为是单向认证,所以服务端并不会要求 Charles 提供证书。

但是实现双向校验的成本会比较高,因为 UA 端的证书管理比较复杂,例如证书的获取、有效期管理等等问题,而且需要用户手动添加到 Trust Store,这样也会降低用户体验。

既然双向认证的成本如此之高,那我们不妨利用 SSL Pinning 来解决证书认证被“劫持”的问题。

OkHttp 在 UA 端用一个类 Pin 来表示服务端的 TLS 证书。

证书的最终的表现形式是一个利用哈希算法(由 hashAlgorithm 字段表示)对证书公钥生成的哈希值(由 hash 字段表示),形式如下:

sha256/afwiKY3RxoMmLkuRW1l7QsPZTJPwDS2pdDROQjXw8ig=

斜杠之前的字符串是 hashAlgorithm,之后的字符串是 hash 值。

TLS 证书的 Extension 字段中有一个 SAN,用于配置域名,例如 「*. http://youzan.com 」的证书中配置了两个域名 —— *. http://youzan.com  和  http://youzan.com ,两者所匹配的域名是不同的,所以 Pin 用了一个 pattern 字段来表示两种模式。

我们知道,TLS 证书携带了端的公钥(Public Key),而这个公钥是 TLS 能够通过握手协商出“对称加密密钥”的关键,证书验证仅仅是为了证明当前证书确实是这个公钥的携带者,或者叫 Owner。所以我们只需要用一个 Pin 把服务端证书的公钥存储在本地,当得到证书链(Certificate Chain)之后,用 Pin 里的 hash 去匹配证书的公钥。

因为本地可以配置多个 Pin,因此 OkHttp 用了一个 CertificatePinner 来管理。

如此一来,在 TLS 握手过程中,校验证书那一步就可以保证服务端下发的证书是客户端想要的,从而避免了被中间人攻击(MIMA),因为本地没有存储中间人证书的 Pin,所以证书匹配会失败,握手也会失败,从而连接无法建立。

Xcode SSL pinning 信任锚证书

【中文标题】Xcode SSL pinning 信任锚证书【英文标题】:Xcode SSL pinning trust anchor certificates 【发布时间】:2017-04-25 17:54:19 【问题描述】:

我不是 iOS 和 SSL 固定专家。尝试将本地证书添加到锚点中以信任它们。

尝试了几个代码,但总是返回 kSecTrustResultRecoverableTrustFailure。

这段代码有什么问题? 我应该将 cer 转换为 der 吗? 我应该删除服务器证书并仅使用本地受信任的证书吗?

任何想法,输入?

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

NSString * derCaPath;
NSMutableArray * chain = [NSMutableArray array];

for(int i=0; i<= 3; i++)

    if (i==0)
        derCaPath = [[NSBundle mainBundle] pathForResource:@"dstrootcax3" ofType:@"cer"];
    if (i==1)
        derCaPath = [[NSBundle mainBundle] pathForResource:@"comodorsacertificationauthority" ofType:@"cer"];
    if (i==2)
        derCaPath = [[NSBundle mainBundle] pathForResource:@"geotrustglobalca" ofType:@"cer"];
    else
        derCaPath = [[NSBundle mainBundle] pathForResource:@"thawteprimaryrootca" ofType:@"cer"];

    NSData *derCA = [NSData dataWithContentsOfFile:derCaPath];

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

    //NSArray * chain = [NSArray arrayWithObject:(__bridge id)(caRef)];

    [chain addObject:(__bridge id)(caRef)];



caChainArrayRef = CFBridgingRetain(chain);

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

    SecTrustRef trust = nil;
    SecTrustResultType result;
    OSStatus err = errSecSuccess;

#if DEBUG
    
        NSLog(@"Chain received from the server (working 'up'):");
        CFIndex certificateCount = SecTrustGetCertificateCount(challenge.protectionSpace.serverTrust);
        for(int i = 0; i < certificateCount; i++) 
            SecCertificateRef certRef = SecTrustGetCertificateAtIndex(challenge.protectionSpace.serverTrust, i);
            //CFStringRef str = SecCertificateCopyLongDescription(kCFAllocatorDefault, certRef, nil);
            //NSLog(@"   %02i: %@", 1+i, str);
            //CFRelease(str);
        

        NSLog(@"Local Roots we trust:");
        for(int i = 0; i < CFArrayGetCount(caChainArrayRef); i++) 
            SecCertificateRef certRef = (SecCertificateRef) CFArrayGetValueAtIndex(caChainArrayRef, i);
            //CFStringRef str = SecCertificateCopyLongDescription(kCFAllocatorDefault, certRef, nil);
            //NSLog(@"   %02i: %@", 1+i, str);
            //CFRelease(str);
        
    
#endif

    if (checkHostname) 
        // We use the standard Policy of SSL - which also checks hostnames.
        // -- see SecPolicyCreateSSL() for details.
        //
        trust = challenge.protectionSpace.serverTrust;
        //
#if DEBUG
        NSLog(@"The certificate is expected to match '%@' as the hostname",
              challenge.protectionSpace.host);
#endif
     else 
        // Create a new Policy - which goes easy on the hostname.
        //

        // Extract the chain of certificates provided by the server.
        //
        CFIndex certificateCount = SecTrustGetCertificateCount(challenge.protectionSpace.serverTrust);
        NSMutableArray * chain = [NSMutableArray array];

        for(int i = 0; i < certificateCount; i++) 
            SecCertificateRef certRef = SecTrustGetCertificateAtIndex(challenge.protectionSpace.serverTrust, i);
            [chain addObject:(__bridge id)(certRef)];
        

        for(int i = 0; i < CFArrayGetCount(caChainArrayRef); i++) 
            SecCertificateRef certRef = (SecCertificateRef) CFArrayGetValueAtIndex(caChainArrayRef, i);
            [chain addObject:(__bridge id)(certRef)];
        


        // And create a bland policy which only checks signature paths.
        //
        if (err == errSecSuccess)
            err = SecTrustCreateWithCertificates((__bridge CFArrayRef)(chain),
                                                 SecPolicyCreateBasicX509(), &trust);
#if DEBUG
        NSLog(@"The certificate is NOT expected to match the hostname '%@' ",
              challenge.protectionSpace.host);
#endif
    ;

    // Explicity specify the list of certificates we actually trust (i.e. those I have hardcoded
    // in the app - rather than those provided by some randon server on the internet).
    //
    if (err == errSecSuccess)
        err = SecTrustSetAnchorCertificates(trust,caChainArrayRef);

    // And only use above - i.e. do not check the system its global keychain or something
    // else the user may have fiddled with.
    //
    if (err == errSecSuccess)
        err = SecTrustSetAnchorCertificatesOnly(trust, YES);

    if (err == errSecSuccess)
        err = SecTrustEvaluate(trust, &result);


    if (err == errSecSuccess) 
        switch (result) 
            case kSecTrustResultProceed:
                // User gave explicit permission to trust this specific
                // root at some point (in the past).
                //
                NSLog(@"GOOD. kSecTrustResultProceed - the user explicitly trusts this CA");
                [challenge.sender useCredential:[NSURLCredential credentialForTrust:trust]
                     forAuthenticationChallenge:challenge];
                goto done;
                break;
            case kSecTrustResultUnspecified:
                // The chain is technically valid and matches up to the root
                // we provided. The user has not had any say in this though,
                // hence it is not a kSecTrustResultProceed.
                //
                NSLog(@"GOOD. kSecTrustResultUnspecified - So things are technically trusted. But the user was not involved.");
                [challenge.sender useCredential:[NSURLCredential credentialForTrust:trust]
                     forAuthenticationChallenge:challenge];
                goto done;
                break;
            case kSecTrustResultInvalid:
                NSLog(@"FAIL. kSecTrustResultInvalid");
                break;
            case kSecTrustResultDeny:
                NSLog(@"FAIL. kSecTrustResultDeny (i.e. user said no explicitly)");
                break;
            case kSecTrustResultFatalTrustFailure:
                NSLog(@"FAIL. kSecTrustResultFatalTrustFailure");
                break;
            case kSecTrustResultOtherError:
                NSLog(@"FAIL. kSecTrustResultOtherError");
                break;
            case kSecTrustResultRecoverableTrustFailure:
                NSLog(@"FAIL. kSecTrustResultRecoverableTrustFailure (i.e. user could say OK, but has not been asked this)");
                // DM 25.04.2017 we allow connection for the moment
                [challenge.sender useCredential:[NSURLCredential credentialForTrust:trust]
                     forAuthenticationChallenge:challenge];
                goto done;
                break;
            default:
                NSAssert(NO,@"Unexpected result: %d", result);
                break;
        
        // Reject.
        [challenge.sender cancelAuthenticationChallenge:challenge];
        goto done;
    ;
    //CFStringRef str =SecCopyErrorMessageString(err,NULL);
    //NSLog(@"Internal failure to validate: result %@", str);
    //CFRelease(str);

    [[challenge sender] cancelAuthenticationChallenge:challenge];

done:
    if (!checkHostname)
        CFRelease(trust);
    return;

// In this example we can cancel at this point - as we only do
// canAuthenticateAgainstProtectionSpace against ServerTrust.
//
// But in other situations a more gentle continue may be appropriate.
//
// [challenge.sender continueWithoutCredentialForAuthenticationChallenge:challenge];

NSLog(@"Not something we can handle - so we're canceling it.");
[challenge.sender cancelAuthenticationChallenge:challenge];

【问题讨论】:

【参考方案1】:

您不应该自己执行此操作:API(如您所见)非常棘手,几乎不可能正确验证,除非您同时是 SSL 和 iOS 专家。

例如,kSecTrustResultRecoverableTrustFailure 可能意味着很多东西,例如过期的证书;您不希望您的应用程序允许这样做。与主机名验证相同:它永远不应该被禁用(即使是调试:很容易生成具有正确主机名的测试证书)。

我开发了一个用于 SSL pinning 的库,它负责处理所有这些事情: https://github.com/datatheorem/TrustKit 。您应该尝试一下,因为它是专门为您的用例设计的。

【讨论】:

Nabla,非常感谢您的回答和您项目的链接。我看了看它,它对我的​​项目有很大的发展。我认为最简单的方法是将我的 cer 证书转换为 der 并比较服务器和本地字节以信任连接。

以上是关于证书链和TLS Pinning的主要内容,如果未能解决你的问题,请参考以下文章

iOS SSL Pinning 没有使用 AFNetworking 的证书?

突破SSL Pinning抓app的数据包

SSL pinning - 在 AFSecurityPolicy 中设置固定公钥而不是固定证书

ios 如何去掉 ssl pinning

可靠地验证 JWS 证书链和域

CA双向认证补充:java客户端使用优化及证书链和Android证书