WultraSSLPinning 1.6.0

WultraSSLPinning 1.6.0

Jan KoberskyJuraj DurechJuraj Durech 维护。



为 iOS 实现动态 SSL pinning

WultraSSLPinning 是一个用 Swift 编写的实现动态 SSL pinning 的库。


简介

SSL pinning(或称公钥,或证书固定)是一种减轻针对安全 HTTP 通信的中间人攻击的技术。iOS 中的典型解决方案是将证书的散列值或证书的确切数据打包到应用程序中,并在 URLSessionDelegate 中验证传入的挑战。这通常工作得很好,但它不幸有一个主要的缺点,即证书的过期日期。证书过期迫使您在证书过期前定期更新应用程序,但即使如此,仍有一部分用户不会自动更新他们的应用程序。因此,使用旧版本的用户的将无法联系应用程序服务器。

动态 SSL pinning 是解决此问题的方法,其中证书指纹列表是安全地从远程服务器下载的。这正是 WultraSSLPinning 库所执行的

  • 管理从远程服务器下载的证书的动态列表
  • 列表中的所有条目都使用您的私钥签名,并在库中使用公钥(我们使用 ECDSA-SHA-256 算法)进行验证
  • 提供简单易用的TLS握手指纹验证。

在使用库之前,还应检查我们的其他相关项目。

安装

要求

  • iOS 11.0+
  • tvOS 11.0+
  • Xcode 14+
  • Swift 5.0+

Swift包管理器

Swift包管理器是一种自动化Swift代码分布的工具,并已集成到swift编译器中。

一旦设置好Swift包,将此库作为依赖项添加,就像将其添加到Package.swift中的dependencies值一样简单。

dependencies: [
    .package(url: "https://github.com/wultra/ssl-pinnin-ios.git", .upToNextMajor(from: "1.5.0"))
]

CocoaPods

CocoaPods是 Cocoa 项目的依赖项管理器。您可以使用以下命令安装它

$ gem install cocoapods

要将框架集成到您的Xcode项目中使用CocoaPods,请在您的Podfile中指定它。

platform :ios, '11.0'
target '<Your Target App>' do
  pod 'WultraSSLPinning/PowerAuthIntegration'
end

当前库的版本依赖于版本0.19.1及更高版本的PowerAuth2框架。

Carthage

注意,Carthage集成是实验性的。我们不提供对此类安装的支持。

Carthage是一个去中心化的依赖管理器,它构建您的依赖项并为您提供了二进制框架。您可以使用以下命令使用Homebrew安装Carthage

$ brew update
$ brew install carthage

要使用Carthage将库集成到您的Xcode项目中,请在您的Cartfile中指定它。

github "wultra/WultraSSLPinning"

运行carthage update以构建框架,并将生成的WultraSSLPinning.framework拖动到您的Xcode项目中。


使用说明

该库提供了以下核心类型

  • CertStore - 主类,提供所有动态固件的任务
  • CertStoreConfiguration - CertStore类的配置结构

本文档的下几章将解释如何为SSL固件目的配置和使用CertStore

配置

以下代码将使用PowerAuth2作为加密提供商和安全存储提供商的基本配置来配置CertStore对象

import WultraSSLPinning

let configuration = CertStoreConfiguration(
    serviceUrl: URL(string: "https://...")!,
    publicKey: "BMne....kdh2ak=",
    useChallenge: true
)
let certStore = CertStore.powerAuthCertStore(configuration: configuration)

我们将在其余的文档中使用certStore变量作为已配置的CertStore实例的参考。

配置有以下属性

  • serviceUrl - 定义证书远程列表的URL的参数。建议serviceUrl指向您想要保护其固件的不同域。有关更多详细信息,请参阅常见问题解答部分。
  • publicKey - 包含与私有密钥对应的公钥,用于数据签名。期望Base64格式化的字符串。
  • useChallenge - 定义远程服务器是否要求挑战请求头
  • expectedCommonNames - 一个可选的字符串数组,定义了您在证书验证中期待哪些域名。
  • identifier - 可选的字符串标识符,用于应用中有多个CertStore实例的场景。
  • fallbackCertificatesData - 可选的硬编码回退指纹数据。有关详细信息,请参阅本文档的下一章。
  • periodicUpdateInterval - 定义CertStore在后台静默更新指纹的时间间隔。默认值为1周。
  • expirationUpdateTreshold - 定义下一个证书到期前的时间窗口。在此时间窗口内,CertStore会尝试比通常情况下更频繁地更新指纹列表。默认值为下一个到期前的2周。
  • sslValidationStrategy - 定义由库本身启动的HTTPS连接的验证策略。默认值.default执行操作系统提供的标准证书链验证。请注意,更改此选项可能会使您的应用面临风险。您不应在SSL验证关闭的状态下将应用部署到生产环境中。

预定义的指纹

CertStoreConfiguration可能包含预定义证书指纹的辅助数据。这种技术可以在指纹数据库为空时加速应用首次启动。一旦回退指纹过期,您仍需要更新您的应用程序。

要配置此属性,您需要提供包含回退指纹的JSON数据。该JSON应包含与从服务器接收的数据相同的数据,但“signature”属性未经验证(但在JSON中必须提供)。例如

{
   "fingerprints":[
      {
         "name": "github.com",
         "fingerprint": "MRFQDEpmASza4zPsP8ocnd5FyVREDn7kE3Fr/zZjwHQ=",
         "expires": 1591185600,
         "signature": ""
      }
   ]
}
""".data(using: .ascii)

let configuration = CertStoreConfiguration(
    serviceUrl: URL(string: "https://...")!,
    publicKey: "BMne....kdh2ak=",
    fallbackCertificatesData: fallbackData!
)
let certStore = CertStore.powerAuthCertStore(configuration: configuration)

请注意,如果您提供错误的JSON数据,则会引发致命错误。

共享实例

库不提供CertStore的单例,但您可以自己实现。例如

extension CertStore {
    static var shared: CertStore {
        let config = CertStoreConfiguration(
            serviceUrl: URL(string: "https://...")!,
            publicKey: "BMne....kdh2ak="
        )
        return .powerAuthCertStore(configuration: config)
    }
}

更新指纹

要从远程服务器更新指纹列表,请使用以下代码

certStore.update { (result, error) in
   if result == .ok {
       // everything's OK, 
       // No action is required, or silent update was started
   } else if result == .storeIsEmpty {
       // Update succeeded, but it looks like the remote list contains
       // already expired fingerprints. The certStore will probably not be able
       // to validate the fingerprints.
   } else {
       // Other error. See `CertStore.UpdateResult` for details.
       // The "error" variable is set in case of a network error.
   }
}

通常需要在应用程序启动时调用更新,在你发起安全HTTP请求到服务器(期望使用PIN进行验证的证书)之前进行。更新函数工作在两种基本模式

  • 阻塞模式,当你的应用程序需要等待下载证书列表时。这在所有证书指纹都已过期或在应用程序首次启动时(例如,没有证书列表)通常会发生
  • 静默更新模式,当回调立即排队到完成队列中,但CertStore在后台执行更新。静默更新的目的是不阻塞你的应用启动,但仍保持指纹列表是最新的。更新的周期性将由CertStore自动确定,但不用担心,我们不想消耗用户的流量计划:)

你可以选择性地提供完成分发队列来安排完成块。这可能在从除了“主”线程之外的地方调用更新时很有用(例如,从你自己的网络代码中)。默认完成队列是.main

指纹验证

CertStore提供了几个用于证书指纹验证的方法。你可以选择最适合你场景的一个

// [ 1 ]  If you already have the common name (e.g. domain) and certificate fingerprint

let commonName = "yourdomain.com"
let fingerprint = Data(...)
let validationResult = certStore.validate(commonName: commonName, fingerprint: fingerprint)

// [ 2 ]  If you already have the common name and the certificate data (in DER format)

let commonName = "yourdomain.com"
let certData = Data(...)
let validationResult = certStore.validate(commonName: commonName, certificateData: certData)

// [ 3 ]  You want to validate URLAuthenticationChallenge

let validationResult = certStore.validate(challenge: challenge)

每个validate方法返回包含以下选项的CertStore.ValidationResult枚举

  • trusted - 服务器证书是可信的。你可以继续通信

    在这种情况下,正确的操作是继续进行当前的TLS握手(例如,向完成回调报告.performDefaultHandling

  • untrusted - 服务器证书不可信。你应该取消当前的挑战。

    不可信的结果意味着CertStore在其数据库中存储了一些指纹,但没有一个与请求验证的值匹配。在这种情况下,正确的操作始终是取消当前的TLS握手(例如,向完成回调报告.cancelAuthenticationChallenge

  • empty - 指纹数据库为空,或者没有验证的通用名指纹。

    “空”验证结果通常意味着 CertStore 应立即更新证书列表。在进行此操作之前,您应该检查请求的通用名是否符合预期。为了简化此步骤,您可以将预期通用名的列表设置在 CertStoreConfiguration 中,并将其他所有内容视为不可信。

    在所有情况下,对此情况的正确响应始终是取消正在进行的 TLS 握手(例如,向完成回调报告 .cancelAuthenticationChallenge

您的应用中完整的挑战处理可能如下所示

class YourUrlSessionDelegate: NSObject, URLSessionDelegate {
    
    let certStore: CertStore
    
    init(certStore: CertStore) {
        self.certStore = certStore
    }
    
    func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
        switch certStore.validate(challenge: challenge) {
        case .trusted:
            // Accept challenge with a default handling
            completionHandler(.performDefaultHandling, nil)
        case .untrusted, .empty:
            /// Reject challenge
            completionHandler(.cancelAuthenticationChallenge, nil)
        }
    }
}

PowerAuth 集成

WultraSSLPinning/PowerAuthIntegration cocoapod 子规范提供了一些额外的类,这些类增强了 PowerAuth SDK 功能。其中最重要的是 PowerAuthSslPinningValidationStrategy 类,该类实现了使用存储在 CertStore 中的指纹实现的 SSL pinning。您可以从现有的 CertStore 简单实例化此对象,并将其设置到 PowerAuthClientConfiguration 中。然后,该类将为从 PowerAuth SDK 启动的所有通信提供 SSL pinning。

例如,如果您想同时使用 PowerAuthSDKCertStore 作为单例,配置顺序可能如下所示:

import WultraSSLPinning
import PowerAuth2

extension CertStore {
    /// Singleton for `CertStore`
    static var shared: CertStore {
        let config = CertStoreConfiguration(
            serviceUrl: URL(string: "https://...")!,
            publicKey: "BASE64...KEY"
        )
        return .powerAuthCertStore(configuration: config)
    }
}

extension PowerAuthSDK {
    /// Singleton for `PowerAuthSDK`
    static var shared: PowerAuthSDK {
        // Configure your PA...
        let config = PowerAuthConfiguration()
        config.baseEndpointUrl = ...
        
        // Configure the keychain
        let keychain = PowerAuthKeychainConfiguration()
        keychain.identifier = ...
        
        // Configure PowerAuthClient and assign validation strategy...
        let client = PowerAuthClientConfiguration()
        client.sslValidationStrategy = CertStore.shared.powerAuthSslValidationStrategy()
        
        // And construct the SDK instance
        guard let powerAuth = PowerAuthSDK(configuration: config, keychainConfiguration: keychain, clientConfiguration: client)
            else { fatalError() }
        return powerAuth
    }
}

常见问题解答

为什么 serviceUrl 使用不同的域名?

iOS 使用 TLS 缓存来对所有远程服务器的安全连接进行缓存。缓存保持已经建立连接一段时间,以加快下一个 HTTPS 请求(更多信息请参阅 Apple 技术问答)。不幸的是,您无法直接控制该缓存,因此无法关闭已建立的连接。这不幸地给攻击者打开了一扇小门。想象以下场景:

  1. 获取远程指纹列表的连接不应该使用巨针保护。必须不惜一切代价访问该列表,因此使用巨针保护可能会使证书存储本身陷入死锁(或者简单地将它移动到下一个级别,在那里您需要更新需要保护获取新指纹列表的指纹)
  2. 通常需要在启动应用程序之前更新指纹列表,在一切其他事情之前。
  3. 由于步骤1,攻击者可以使用他的虚假CA诱导您的应用程序获取证书列表。这不会允许他将新条目插入列表,但这不是重点。
  4. 如果您的API位于同一域,则您应用程序的连接将重用已经建立的连接(在第2步或第3步中打开),通过中间人进行。就是这样。

嗯,不是一切都没有失去。如果您使用URLSession(可能是的),则可以重新创建一个新的URLSession,因为它有自己的TLS缓存。但是,所有这些都是没有良好记录的,因此我们建议将指纹列表放在不同域名上,以避免TLS缓存中这类冲突。

库可以提供更多的调试信息吗?

是的,您可以更改打印到调试控制台的信息量

WultraDebug.verboseLevel = .all

为什么依赖于PowerAuth2?

库需要几个加密原语,这些通常在iOS中不可用(如ECDSA)。PowerAuth2已经提供了这些函数,而且我们的大多数客户已经在他们的应用程序中使用了PowerAuth2框架。因此,为了我们的目的,将这两个库粘合在一起是有意义的。

但是,并非一切都没有失去。库的核心使用CryptoProvider协议,因此具有实现独立性。我们将在以后提供独立的巨针库版本。


许可证

所有来源都采用Apache 2.0许可证进行许可。您可以无限制地使用它们。如果您正在使用这个库,请告诉我们。我们将很乐意分享和推广您的项目。

联系

如果您需要任何帮助,请随时给我们发邮件至[email protected]或我们的官方gitter.im/wultra频道。

安全披露

如果您相信您发现了WultraSSLPinning的安全漏洞,您应尽快通过电子邮件向[email protected]报告。请不要将其发布到公共问题跟踪器。