Alamofire serverTrustEvaluationFailed 错误分析

serverTrustEvaluationFailed 错误

1
2
3
4
5
6
7
8
Printing description of error:
▿ AFError
▿ serverTrustEvaluationFailed : 1 element
▿ reason : ServerTrustFailureReason
▿ noRequiredEvaluator : 1 element
- host : "***"
(lldb) po error.debugDescription
"Server trust evaluation failed due to reason: A ServerTrustEvaluating value is required for host *** but none was found."

原因是配置了白名单

1
2
3
4
5
6
7
8
9
10
11
var evaluators: [String: ServerTrustEvaluating] = [:]

let evaluators: [String: ServerTrustEvaluating] = [
"*.yourdomain.com": PinnedCertificatesTrustEvaluator()
]
// 白名单 allHostsMustBeEvaluated: true
let serverTrust = ServerTrustManager(allHostsMustBeEvaluated: true,
evaluators: evaluators)

session = Alamofire.Session(configuration: configuration,
serverTrustManager: serverTrust)

可把 allHostsMustBeEvaluated 改为 false,只对指定的host开启。

原理 TLS Server Trust

https://github.com/Alamofire/Alamofire/blob/master/Documentation/AdvancedUsage.md#evaluating-server-trusts-with-servertrustmanager-and-servertrustevaluating

在与服务器和 Web 服务通信时使用安全的 HTTPS 连接是保护敏感数据的重要步骤。默认情况下,Alamofire 会收到与 URLSession 相同的自动 TLS 证书和证书链验证。虽然这保证了证书链的有效性,但它并不能防止中间人 (MITM) 攻击或其他潜在漏洞。为了减轻中间人攻击,处理敏感客户数据或财务信息的应用程序应使用 Alamofire ServerTrustEvaluating 协议提供的证书或公钥固定。

使用 ServerTrustManager 和 ServerTrustEvaluating 评估服务器信任

该协议 ServerTrustEvaluating 提供了一种执行任何类型的服务器信任评估的方法。它只有一个要求:

1
func evaluate(_ trust: SecTrust, forHost host: String) throws

此方法提供从基础 URLSession 接收 SecTrust 的值和主机 String ,并提供执行各种评估的机会。

包括许多不同类型的信任评估器,提供对评估过程的可组合控制:

  1. DefaultTrustEvaluator :使用默认服务器信任评估,同时允许您控制是否验证质询提供的主机。
  2. RevocationTrustEvaluator :检查收到的证书的状态,以确保其未被吊销。由于它需要网络请求开销,因此通常不会对每个请求执行此操作。
  3. PinnedCertificatesTrustEvaluator :使用提供的证书来验证服务器信任。如果其中一个固定的证书与其中一个服务器证书匹配,则认为服务器信任有效。此赋值器还可以接受自签名证书。
  4. PublicKeysTrustEvaluator :使用提供的公钥来验证服务器信任。如果其中一个固定的公钥与其中一个服务器证书公钥匹配,则认为服务器信任有效。
  5. CompositeTrustEvaluator :计算值数组,仅当所有 ServerTrustEvaluating 值都成功时才成功。例如,此类型可用于组合 RevocationTrustEvaluator 和 PinnedCertificatesTrustEvaluator 。
  6. DisabledTrustEvaluator :此评估程序应仅在调试方案中使用,因为它会禁用所有评估,而这些评估将始终将任何服务器信任视为有效。此评估器绝不应在生产环境中使用!

ServerTrustManager

负责 ServerTrustManager 存储值到特定主机的 ServerTrustEvaluating 内部映射。这允许 Alamofire 使用不同的评估器评估每个主机。

1
2
3
4
5
6
7
8
let evaluators: [String: ServerTrustEvaluating] = [
// 默认情况下,应用程序捆绑包中包含的证书会自动固定。
"cert.example.com": PinnedCertificatesTrustEvaluator(),
// 默认情况下,会自动使用应用程序包中包含的证书中的公钥。
"keys.example.com": PublicKeysTrustEvaluator(),
]

let manager = ServerTrustManager(evaluators: evaluators)

这将 ServerTrustManager 具有以下行为:

  1. cert.example.com 将始终使用启用默认和主机验证的证书固定,因此需要满足以下条件才能使 TLS 握手成功:
    1. 证书链必须有效。
    2. 证书链必须包含其中一个固定的证书。
    3. 质询主机必须与证书链的叶证书中的主机匹配。
  2. keys.example.com 将始终使用启用默认和主机验证的公钥固定,因此需要满足以下条件才能使 TLS 握手成功:
    1. 证书链必须有效。
    2. 证书链必须包含其中一个固定的公钥。
    3. 质询主机必须与证书链的叶证书中的主机匹配。
  3. 对其他主机的请求将产生错误,因为 ServerTrustManager 默认情况下需要评估所有主机。

测试用例

Alamofire 项目中测试用例:Tests/TLSEvaluationTests.swift

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
private enum TestCertificates {
static let rootCA = TestCertificates.certificate(filename: "expired.badssl.com-root-ca")
static let intermediateCA1 = TestCertificates.certificate(filename: "expired.badssl.com-intermediate-ca-1")
static let intermediateCA2 = TestCertificates.certificate(filename: "expired.badssl.com-intermediate-ca-2")
static let leaf = TestCertificates.certificate(filename: "expired.badssl.com-leaf")

// 从给定的文件名创建证书:SecCertificate对象。
static func certificate(filename: String) -> SecCertificate {
let filePath = Bundle.test.path(forResource: filename, ofType: "cer")!
let data = try! Data(contentsOf: URL(fileURLWithPath: filePath))
let certificate = SecCertificateCreateWithData(nil, data as CFData)!

return certificate
}
}

func testThatExpiredCertificateRequestFailsWhenPinningLeafPublicKeyWithCertificateChainValidation() {
// Given
// 这里直接从证书提取公钥
let keys = [TestCertificates.leaf].af.publicKeys
let evaluators = [expiredHost: PublicKeysTrustEvaluator(keys: keys)]

let manager = Session(configuration: configuration,
serverTrustManager: ServerTrustManager(evaluators: evaluators))

let expectation = expectation(description: "\(expiredURLString)")
var error: AFError?

// When
manager.request(expiredURLString)
.response { resp in
error = resp.error
expectation.fulfill()
}

waitForExpectations(timeout: timeout)

// Then
XCTAssertNotNil(error, "error should not be nil")
XCTAssertEqual(error?.isServerTrustEvaluationError, true)

if case let .serverTrustEvaluationFailed(reason)? = error {
if #available(iOS 12, macOS 10.14, tvOS 12, watchOS 5, *) {
XCTAssertTrue(reason.isTrustEvaluationFailed, "should be .trustEvaluationFailed")
} else {
XCTAssertTrue(reason.isDefaultEvaluationFailed, "should be .defaultEvaluationFailed")
}
} else {
XCTFail("error should be .serverTrustEvaluationFailed")
}
}