【问题标题】:iOS TLS/SSL Pinning using NSRequiresCertificateTransparency key in Info.plistiOS TLS/SSL Pinning 在 Info.plist 中使用 NSRequiresCertificateTransparency 键
【发布时间】:2021-10-18 13:39:34
【问题描述】:

我想使用 SSL Pinning 保护我的应用免受中间人 (mitm) 攻击。
默认情况下,可以使用 Charles 或 mitmproxy 等代理来拦截流量,并使用自签名证书对其进行解密。

经过大量研究,我找到了几个选择:

  1. NSPinnedDomains > MY_DOMAIN > NSPinnedLeafIdentities 添加到Info.plist
    苹果文档:Identity Pinning
    卫士广场:Leveraging Info.plist Based Certificate Pinning
    优点:简单
    缺点:一旦更新证书/私钥(通常在几个月后),应用程序将无法使用

  2. NSPinnedDomains > MY_DOMAIN > NSPinnedCAIdentities 添加到Info.plist
    苹果文档:same as above
    优点:简单。叶证书续订没有失败,因为根 CA 被固定(过期日期为几十年)
    缺点:似乎是多余的,因为大多数根 CA 都是 already included in the OS

  3. 检查代码 URLSessionDelegate > SecTrustEvaluateWithError(或 Alamofire 包装器)中的证书
    雷·温德利希:Preventing Man-in-the-Middle Attacks in iOS with SSL Pinning
    苹果文档:Handling an Authentication Challenge
    中篇:Everything you need to know about SSL Pinning
    中篇:Securing iOS Applications with SSL Pinning
    优点:更灵活。可能更安全。 Apple 推荐(请参阅上面的 Apple 链接)。
    缺点:(1)或(2)的一个更费力的版本。关于叶子到期/根 CA 冗余的缺点与 (1) 和 (2) 相同。更复杂。

  4. 将 NSExceptionDomains > MY_DOMAIN > NSRequiresCertificateTransparency 添加到 Info.plist
    苹果文档:Section Info.plist keys 'Certificate Transparency'
    优点:非常简单。没有多余的 CA 集成。
    缺点:文档不清楚这是否应该用于 ssl pinning

经过评估,我得出以下结论:

  1. 由于证书过期,不适合生产应用
  2. 可能是简单性、安全性和可持续性之间的最佳平衡 - 但我不喜欢重复添加系统已经知道的根 CA
  3. 太复杂,太冒险,任何实现错误都可能锁死应用
  4. 我的首选方式。简单的。在我的测试中有效,但文档不明确。

我很想使用选项 (4),但我不确定这是否真的适用于 ssl pinning。

在文档中它说:

证书透明度 (CT) 是 ATS 可用于识别错误或恶意颁发的 X.509 证书的协议。将 NSRequiresCertificateTransparency 键的值设置为 YES 以要求对于给定域,服务器证书由来自 Apple 信任的至少两个 CT 日志的有效签名 CT 时间戳支持。有关证书透明度的更多信息,请参阅 RFC6962。

在链接的 RFC6962 中:

本文档描述了一个实验性协议,用于公开记录传输层安全 (TLS) 证书的存在 [...]

“实验性协议”和“公开记录”这两个术语对我来说很重要,尽管在 Info.plist 中启用该功能似乎可以解决 SSL 固定问题,但我不确定是否应该使用它。
我绝不是安全专家,我需要一个非常简单的解决方案,它可以为我提供体面的保护,同时防止我自己的应用程序因可能过期/更改的证书而窒息。

我的问题:

我应该使用NSRequiresCertificateTransparency 来固定我的应用程序并防止中间人攻击吗?

如果不是:

我应该改用什么?


PS:

这个帖子基本上已经问过同样的问题:
https://developer.apple.com/forums/thread/675791

但是关于NSRequiresCertificateTransparency(4. 在我上面的列表中)的答案很模糊:

没错,Certificate Transparency 是一个很好的工具,可以通过 TLS 扩展(可以在 Packet Trace 中看到)验证提供的叶子是否包含一组嵌入在证书 (RFC 6962) 中的 SCT(签名证书时间戳) ),或通过检查证书的 OCSP 日志。当您在应用中做出信任决定时,我建议您通过 SecPolicyRef 对象查看 is 属性。

补充说明:

我对 Apple 作为一家具有安全意识的公司的期望是,默认情况下会启用固定到根 CA,并且我必须手动添加例外,例如允许在调试版本中使用 Charles 代理。 我听说 Android 就是这样做的。

【问题讨论】:

    标签: ios swift certificate-pinning public-key-pinning sslpinning


    【解决方案1】:

    我正在使用 SecTrustEvaluateWithError 来评估证书。如果证书过期或任何其他评估返回错误的情况,我会从服务器获取新的。证书被存储和接收女巫钥匙串。我遇到此解决方案的一个问题是更新钥匙串中的现有证书,因为在苹果文档中这样做是使用 kSecValueRef 但每当您尝试更新它时都会返回错误。而是将证书与 kSecValueData 一起保存。

    所以这里使用了解决方案 nr 3(一种),但在我的例子中,有一个套接字连接。

    首先我使用 CocoaAsyncSocket 库通过设置连接到套接字

        GCDAsyncSocketManuallyEvaluateTrust: NSNumber(value: true),
        kCFStreamSSLPeerName as String: NSString("name")
    

    接下来我使用委托来接收信任对象

    public func socket(_ sock: GCDAsyncSocket, didReceive trust: SecTrust, completionHandler: @escaping (Bool) -> Void) 
    

    下一步使用现有的(来自钥匙串)评估,或更新证书并重复评估

    if let cert = CertificateManager.shared.getServerCertificate() {
        SecTrustSetAnchorCertificates(trust, [cert] as NSArray)
        SecTrustSetAnchorCertificatesOnly(trust, true)
        var error: CFError?
        let evaluationSucceeded = SecTrustEvaluateWithError(trust, &error)
    
        guard evaluationSucceeded else {
            CertificateManager.shared.updateCertificate()
            return
        }
    
        completionHandler(evaluationSucceeded)
    } else {
        CertificateManager.shared.updateCertificate()
    }
    

    获取证书的方法只是域上具有 URLSessionDelegate 证书的常规URLSession dataTask 我从该对象中获取URLAuthenticationChallenge,您可以检索证书并将其保存到钥匙串。

    有来自苹果文档how to store certificate的信息

    最好通读一遍,但是我上面提到的我在更新现有解决方案时遇到了问题,因此我可以使用一些方法来保存和检索证书

    添加为数据:

    public func saveServerCertificate(_ certificate: SecCertificate, completion: @escaping () -> Void) throws {
            let query: [String: Any] = [kSecClass as String: kSecClassCertificate,
                                        kSecAttrLabel as String: attribute]
    
            let status = SecItemCopyMatching(query as CFDictionary, nil)
            switch status {
            case errSecItemNotFound:
                let certData = SecCertificateCopyData(certificate) as Data
                let saveQuery: [String: Any] = [kSecClass as String: kSecClassCertificate,
                                                kSecAttrLabel as String: attribute,
                                                kSecValueData as String: certData]
                let addStatus = SecItemAdd(saveQuery as CFDictionary, nil)
                guard addStatus == errSecSuccess else { throw KeychainError.unhandledError(status: status) }
                completion()
            case errSecSuccess:
                let certData = SecCertificateCopyData(certificate) as Data
                let attributes: [String: Any] = [kSecValueData as String: certData]
                let updateStatus = SecItemUpdate(query as CFDictionary, attributes as CFDictionary)
                guard updateStatus == errSecSuccess else { throw KeychainError.unhandledError(status: status) }
                completion()
            default:
                throw KeychainError.unhandledError(status: status)
            }
        }
    

    获取并创建带有数据的证书:

    public func getServerCertificate(completion: @escaping (SecCertificate) -> Void) throws {
    
        let query: [String: Any] = [kSecClass as String: kSecClassCertificate,
                                    kSecAttrLabel as String: attribute,
                                    kSecReturnAttributes as String: true,
                                    kSecMatchLimit as String: kSecMatchLimitOne,
                                    kSecReturnData as String: true]
        
        var item: CFTypeRef?
        let status = SecItemCopyMatching(query as CFDictionary, &item)
    
        switch status {
        case errSecItemNotFound:
            throw KeychainError.noCertificate
        case errSecSuccess:
            guard let existingItem = item as? [String : Any],
                  let certData = existingItem[kSecValueData as String] as? Data
            else {
                throw KeychainError.unexpectedCertificateData
            }
    
            if let certificate = SecCertificateCreateWithData(nil, certData as CFData) {
                completion(certificate)
            } else {
                throw KeychainError.unexpectedCertificateData
            }
        default:
            throw KeychainError.unhandledError(status: status)
        }
    }
    

    【讨论】:

    • 您介意分享一些概述此过程的(伪)代码吗?
    【解决方案2】:

    您的其他资源是 OWASP。最好遵循他们对所有平台的建议。

    https://owasp.org/www-community/controls/Certificate_and_Public_Key_Pinning

    至于你的观点:

    1. 证书更改时,公钥并不总是更改(叶证书通常每年轮换一次?)。这是固定公钥的优势之一。

    2. '缺点:似乎是多余的,因为大多数根 CA 已经包含在操作系统中' 正如您在此处所说,固定根证书是没有意义的,因为该证书可能已经被操作系统信任。

    但是,从您链接到的文档中:“固定的 CA 公钥必须出现在中间证书或根证书中的证书链中。固定密钥始终与域名相关联,除非满足固定要求,否则应用将拒绝连接到该域。'

    您将在此处锁定中间证书。为了安心,您可以进行测试以打印出您的根证书 + 中间证书的公钥并证明它们不匹配。

    1. '太复杂,太冒险,任何实施错误都可能锁定应用程序。' Apple 在他们的技术说明中提供了一个实现,您可以自己手动测试所有代码路径。此外,正如 owasp 建议的那样,您可以查看信任工具包。我似乎有一个实现,在这里你可以选择只针对中间证书(非根)与叶节点证书。中级证书的有效期通常为 5-10 年。

    2. 我个人会推迟,因为我不确定,我认为这可能只是除了证书固定之外您可能还想使用的额外检查。 owasp 文档中似乎也没有提到这一点。

    就个人而言,如果我正在编写一个新应用程序,我会选择选项 1。由于 android N,操作系统提供了类似的方法,这意味着你也可以与你的 android 同行保持同步,他们也只需要当你这样做时更新,反之亦然。 https://developer.android.com/training/articles/security-config.html#CertificatePinning

    我不是安全专家,但我是根据我为大型合作公司工作的经验提出的想法,这些公司的应用程序渗透测试。如果你真的在开发一个需要高安全性的应用程序,你真的应该让一个渗透测试人员来测试你的应用程序。如果你在一家大公司,你可能有一个可以提供帮助的网络团队。

    【讨论】:

      【解决方案3】:

      不,您不能使用 NSRequiresCertificateTransparency 进行 ssl 证书锁定,它用于客户端 TLS。如果要实现 pinning,可以使用服务器证书 pining 来防止 MITM 攻击。

      Certificate pining

      区别如下

      1) 客户端证书透明度

      对于 iOS 应用,开启客户端证书透明度检查相当简单——您什么都不做!在运行 iOS 12.1.1 及更高版本的设备上,证书透明度为 enforced by default。对于运行早期版本 iOS 的设备,您需要在 Info.plist 文件中将 NSRequiresCertificateTransparency 选项设置为 YES。

      2) 服务器端证书透明度

      证书透明度有两个方面:

      1. 固定证书:您可以下载服务器的证书并将其捆绑到您的应用中。在运行时,应用会将服务器的证书与您嵌入的证书进行比较。

      2. 固定公钥:您可以检索证书的公钥并将其作为字符串包含在您的代码中。在运行时,应用会将证书的公钥与代码中硬编码的公钥进行比较。

      肯定需要带有 SCT 的 SSL 证书。确保您的服务器证书是具有有效 SCT 的证书。如今,几乎每个 CA 都颁发带有 SCT 的证书

      【讨论】:

      • 你确定 NSRequiresCertificateTransparency 默认是开启的吗?苹果文档清楚地说“默认值为否”。 (developer.apple.com/documentation/bundleresources/…)。您发布的那个链接说“被评估为受信任”。但并没有说它会被评估。仅当 NSRequiresCertificateTransparency 设置为 YES 时才会发生这种情况。
      • @Jacob 是的,但适用于 iOS 12.1.1 以上
      • @JayeshPatel 您能否提供有关 1 的文档/更多信息。 - 我已经尝试过了,它似乎有效,我无法再读取 Charles 的流量了。
      • @de pining 服务器证书更安全,并且与系统默认评估服务器可信度相比,可以防止 MITM 攻击。
      • @JayeshPatel 你的第一个链接格式错误
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2022-06-11
      • 2017-07-26
      • 1970-01-01
      • 2017-06-14
      • 2019-10-31
      • 2015-02-25
      • 2020-04-06
      相关资源
      最近更新 更多