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
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
|
//
// SSLPinningURLSessionDelegate.swift
// MullvadREST
//
// Created by pronebird on 17/05/2021.
// Copyright © 2025 Mullvad VPN AB. All rights reserved.
//
import Foundation
import MullvadLogging
import Network
import Security
final class SSLPinningURLSessionDelegate: NSObject, URLSessionDelegate, @unchecked Sendable {
private let sslHostname: String
private let trustedRootCertificates: [SecCertificate]
private let addressCache: REST.AddressCache
private let logger = Logger(label: "SSLPinningURLSessionDelegate")
init(sslHostname: String, trustedRootCertificates: [SecCertificate], addressCache: REST.AddressCache) {
self.sslHostname = sslHostname
self.trustedRootCertificates = trustedRootCertificates
self.addressCache = addressCache
}
// MARK: - URLSessionDelegate
func urlSession(
_ session: URLSession,
didReceive challenge: URLAuthenticationChallenge,
completionHandler: @escaping @Sendable (URLSession.AuthChallengeDisposition, URLCredential?) -> Void
) {
if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust,
let serverTrust = challenge.protectionSpace.serverTrust
{
/// If a request is going through a local shadowsocks proxy, the host would be a localhost address,`
/// which would not appear in the list of valid host names in the root certificate.
/// The same goes for direct connections to the API, the host would be the IP address of the endpoint.
/// Certificates, cannot be signed for IP addresses, in such case, specify that the host name is `defaultAPIHostname`
var hostName = challenge.protectionSpace.host
let overridenHostnames = [
"\(IPv4Address.loopback)",
"\(IPv6Address.loopback)",
"\(REST.defaultAPIEndpoint.ip)",
"\(addressCache.getCurrentEndpoint().ip)",
]
if overridenHostnames.contains(hostName) {
hostName = sslHostname
}
if verifyServerTrust(serverTrust, for: hostName) {
completionHandler(.useCredential, URLCredential(trust: serverTrust))
return
}
}
completionHandler(.rejectProtectionSpace, nil)
}
// MARK: - Private
private func verifyServerTrust(_ serverTrust: SecTrust, for sslHostname: String) -> Bool {
var secResult: OSStatus
// Set SSL policy
let sslPolicy = SecPolicyCreateSSL(true, sslHostname as CFString)
secResult = SecTrustSetPolicies(serverTrust, sslPolicy)
guard secResult == errSecSuccess else {
logger.error("SecTrustSetPolicies failure: \(formatErrorMessage(code: secResult))")
return false
}
// Set trusted root certificates
secResult = SecTrustSetAnchorCertificates(serverTrust, trustedRootCertificates as CFArray)
guard secResult == errSecSuccess else {
logger.error(
"SecTrustSetAnchorCertificates failure: \(formatErrorMessage(code: secResult))"
)
return false
}
// Tell security framework to only trust the provided root certificates
secResult = SecTrustSetAnchorCertificatesOnly(serverTrust, true)
guard secResult == errSecSuccess else {
logger.error(
"SecTrustSetAnchorCertificatesOnly failure: \(formatErrorMessage(code: secResult))"
)
return false
}
var error: CFError?
if SecTrustEvaluateWithError(serverTrust, &error) {
return true
} else {
logger.error(
"SecTrustEvaluateWithError failure: \(error?.localizedDescription ?? "<nil>")"
)
return false
}
}
private func formatErrorMessage(code: OSStatus) -> String {
let message = SecCopyErrorMessageString(code, nil) as String? ?? "<nil>"
return "\(message) (code: \(code))"
}
}
|