使用Approov在iOS上的动态SSL钉子的工作原理及工作流程

312 阅读6分钟

Approov PoC

Approov的好处之一是,它允许你从你的移动应用程序中删除嵌入式客户秘密。相反,你可以在移动应用的API请求中附加一个短暂的ApproovJSON Web Token(JWT)令牌。由Approov云服务签发,你的API网关会验证该令牌并允许请求继续进行(或不进行)。我们试用了Approov的解决方案,并在Android和iOS上实施了一个PoC。

它的工作原理如下。

Approov flow

为了支持Approov,我们所做的改动并不大,但比典型的SDK要大,因为在SDK中你只需在应用启动时调用一个配置函数。

  • 在应用程序中用假的客户秘密替换掉
  • 在应用启动时配置Approov SDK--使用嵌入式初始配置文件和动态配置
  • 从SDK中获取Approov令牌并将其附加到发出的请求中
  • 存储更新的Approov动态配置

Approov动态配置允许我们进行动态钉住,这是我在这篇博文中想要关注的内容。

但首先,什么是pinning,有没有什么陷阱?

移动客户端可以通过验证服务器的证书/公钥,在允许请求继续进行之前,检查它是否在与一个真正的服务器对话。这种保护机制被OWASP(可能是移动应用安全的主要咨询者)在他们的移动应用安全验证标准中推荐为2级 "深度防御 "保护。

MASVS Levels

L2引入了先进的安全控制,超越了标准要求。为了实现MASVS-L2,必须存在一个威胁模型,而且安全必须是应用的架构和设计的一个组成部分。基于威胁模型,正确的MASVS-L2控制应该已经被选择并成功实施。这个级别适合于处理高度敏感数据的应用程序,如移动银行应用程序。

OWASP MASVS Network

通常情况下,服务器证书/公钥被开发者嵌入到应用程序的二进制文件中(例如,作为一个pem文件或源代码中的硬编码引脚)。在你的应用程序中嵌入一个密钥或证书意味着,如果服务器端的证书发生变化无论是通过正常的证书轮换还是由于安全漏洞,它都可能无法发出请求(哦,哦)。为了防止这种情况,应用程序开发人员需要确保发布的应用程序包含最新的服务器证书。他们可以通过包括多个证书和/或确保他们在服务器上上线前发布带有 "下一个 "服务器证书的应用程序来做到这一点。但这很难管理,而且潜在的麻烦也很大。

另一种方法是动态地做Approov可以方便地做到这一点。

Approov approach to dynamic pinning

动态钉住(用Approov)

Approov cli工具允许你添加域,例如:https://my-api-gateway.my-domain.com 。当域名被添加时,Approov也会获取该域名的证书公钥引脚。

当应用程序启动时,它从Approov请求更新配置,其中包含被钉住的域的公钥插脚。当向API网关发出请求时,移动客户端代码将从Approov SDK中获取针脚,并对照针脚验证网关服务器的证书公钥信息。如果验证成功,请求将继续进行,否则将失败。

下面是Approov文档中的一个实现销钉的示例URLSession

func urlSession(_ session: URLSession,
    didReceive challenge: URLAuthenticationChallenge,
    completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {

    let protectionSpace = challenge.protectionSpace

    // only handle requests that are related to server trust
    if protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust {
        // determine the host whose connection is pinned (this requires that the session delegate keeps a reference
        // to the URLSessionTask when the task is created)
        guard let urlSessionTask = sessionTask,
            let host = urlSessionTaskHost(urlSessionTask: urlSessionTask),
            let pins = Approov.getPins("public-key-sha256")?[host]
        else {
            completionHandler(.cancelAuthenticationChallenge, nil);
            return
        }

        // check the validity of the server trust
        let sslPolicy = SecPolicyCreateSSL(true, host as CFString)
        var secResult = SecTrustResultType.invalid
        guard let serverTrust = protectionSpace.serverTrust,
            // Ensure a sensible SSL policy is used when evaluating the server trust
            SecTrustSetPolicies(serverTrust, sslPolicy) == errSecSuccess,
            SecTrustEvaluate(serverTrust, &secResult) == errSecSuccess,
            secResult == .unspecified || secResult == .proceed
        else {
            completionHandler(.cancelAuthenticationChallenge, nil);
            return
        }

        // remember the hashes of all public key infos for logging in case of pinning failure
        var spkiHashesBase64 = [String](repeating: "", count: SecTrustGetCertificateCount(serverTrust))

        // check public key hash of all certificates in the chain, leaf certificate first
        for i in 0 ..< SecTrustGetCertificateCount(serverTrust) {

            guard let serverCert = SecTrustGetCertificateAtIndex(serverTrust, i),
                let spkiHashBase64 = publicKeyInfoSHA256Base64(certificate: serverCert)
            else {
                continue
            }

            spkiHashesBase64[i] = spkiHashBase64
            NSLog("URLSessionDelegate: host %@ public key hash %@", host, spkiHashesBase64[i])

            // check that the hash is the same as at least one of the pins
            for pin in pins {
                if spkiHashesBase64[i].elementsEqual(pin) {
                    NSLog("URLSessionDelegate: pinning valid")
                    completionHandler(.useCredential, URLCredential(trust: serverTrust))
                    // Successful match
                    return
                }
            }
        }

        // the certificates did not match any of the pins
        completionHandler(.cancelAuthenticationChallenge, nil);

        // log the hashes of all certificates in the certificate chain for the host - this helps with choosing the
        // correct hash(es) to put into the Approov configuration
        for i in 0 ..< SecTrustGetCertificateCount(serverTrust) {
            NSLog("URLSessionDelegate: pinning invalid for host %@ and certificate %d's public key info hash %@",
                  host, i, spkiHashesBase64[i])
        }
    }
}

嗯,这段代码在做什么?

  • urlSession(_:didReceive:completionHandler:)方法将在会话与使用SSL或TLS的远程服务器建立连接时被调用,以允许你的应用程序验证服务器的证书链。
  • Approov.getPins("public-key-sha256")?[host] 的调用是我们为该host ,例如:https://my-api-gateway.my-domain.com ,在动态配置中提供的引脚。
  • 然后遍历服务器提供的证书,检查是否至少有一个与引脚匹配if spkiHashesBase64[i].elementsEqual(pin)
  • 如果有一个匹配的,完成处理程序将被调用,.useCredential ,传入证书,连接将继续👍
  • 如果没有匹配,连接将被取消 ⛔️

运行中的动态引脚

为了测试钉住,你可以使用查尔斯代理设备的SSL连接到钉住的API端点,例如:https://my-api-gateway.my-domain.com

尝试在https://my-api-gateway.my-domain.com 上调用一个返回秘密的API。比如返回OAuth令牌的登录API,并观察API响应中的秘密,你不能这样做,因为API请求在客户端被取消了!这是因为查尔斯代理没有提供与我们的密码相匹配的服务器证书。

现在在查尔斯中禁用SSL代理,并重新尝试。登录成功了!

请注意,引脚的实现是在应用程序的代码中,而不是在Approov SDK的代码中,所以没有Approov也可以做非动态引脚,但引脚管理会更容易出错,而且引脚必须被嵌入,因此可能被坏人从应用程序中提取。

应用程序也可以在没有Approov的情况下做动态引脚,但他们需要托管一个后台服务,以管理域名和证书,并向应用程序提供更新的引脚,所以这不是一个简单的实现。

关于Approov的最后说明:即使应用程序已经实现了Pinning,也有可能在越狱的设备上禁用它这是一个留给读者的练习