summaryrefslogtreecommitdiffhomepage
path: root/ios/MullvadREST/ApiHandlers/SSLPinningURLSessionDelegate.swift
blob: 72ac430d95f72edb4667dc49401b70445809511c (plain)
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))"
    }
}