1

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

经过广泛的研究,我发现了几种选择:

  1. 添加NSPinnedDomains > MY_DOMAIN > NSPinnedLeafIdentitiesInfo.plist
    Apple 文档:Identity Pinning
    Guardsquare:利用基于 Info.plist 的证书固定
    优点:简单
    缺点:一旦更新证书/私钥(通常在几个月后),应用程序将无法使用

  2. 添加NSPinnedDomains > MY_DOMAIN > NSPinnedCAIdentitiesInfo.plist
    Apple 文档:与上述相同
    优点:简单。叶证书更新没有失败,因为根 CA 被固定(过期日期为几十年)
    缺点:似乎多余,因为大多数根 CA已经包含在操作系统中

  3. 检查代码中的证书URLSessionDelegate> SecTrustEvaluateWithError(或 Alamofire 包装器)
    Ray Wenderlich:使用 SSL Pinning 防止 iOS 中的中间人攻击
    Apple 文档:处理身份验证挑战
    Medium 文章:关于 SSL Pinning 需要了解的一切
    Medium 文章:保护 iOS具有 SSL Pinning
    优点的应用程序:更大的灵活性。可能更安全。Apple 推荐(参见上面的 Apple 链接)。
    缺点:(1)或(2)的一个更费力的版本。关于叶子到期/根 CA 冗余的缺点与 (1) 和 (2) 相同。更复杂。

  4. 添加 NSExceptionDomains > MY_DOMAIN > NSRequiresCertificateTransparency 到 Info.plist
    Apple 文档:部分 Info.plist 键“证书透明度”
    优点:非常简单。没有多余的 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在我的应用程序上使用 ssl pinning 和防止 mitm-attacks 吗?

如果不:

我应该改用什么?


PS:

在这个线程中基本上已经提出了同样的问题:
https ://developer.apple.com/forums/thread/675791

然而,答案是模糊的NSRequiresCertificateTransparency(4。在我上面的列表中):

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

附加说明:

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

4

3 回答 3

2

我正在使用 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()
}

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

苹果文档中有关于如何存储证书的信息

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

添加为数据:

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)
    }
}
于 2021-10-22T05:55:18.310 回答
1

您的其他资源是 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

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

于 2021-10-20T10:44:59.393 回答
1

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

证书锁定

区别如下

1)客户端证书透明

对于 iOS 应用程序,打开客户端证书透明度检查相当简单——你什么都不做!默认情况下,证书透明度在运行 iOS 12.1.1 及更高版本的设备上强制执行。对于运行早期版本 iOS 的设备,您需要在 Info.plist 文件中将 NSRequiresCertificateTransparency选项设置为 YES。

2)服务器端证书透明

证书透明度有两个方面:

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

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

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

于 2021-10-20T11:36:34.963 回答