launchdarkly / swift-eventsource

Server-sent events (SSE) client implementation in Swift for iOS, macOS, tvOS, and watchOS
https://launchdarkly.github.io/swift-eventsource/
Other
193 stars 36 forks source link

How to pin SSL certificate with EventSource? #85

Open houmie opened 1 week ago

houmie commented 1 week ago

Great project but there is a weakness, how do we pin a SSL certificate? Otherwise anyone could just use Proxyman and do a man-in-the-middle attack to see what the app is sending out (such as api key etc)

I can use this code below with a normal URL connection and pin the certificate down like that. But how can I integrate this urlSession with swift-EventSource? I couldn't find a way yet...

func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
        // Helper function to check for subdomains
        func containsSubdomain(host: String, subdomains: [String]) -> Bool {
            // Split the host into components
            let hostComponents = host.split(separator: ".")

            // Ensure there are enough components to check for subdomain
            guard hostComponents.count > 2 else { return false }

            // The subdomain is the first component of the host
            let subdomain = String(hostComponents[0])

            // Check if the subdomain matches any of the given subdomains
            return subdomains.contains(subdomain)
        }

        // Extract the host from the protection space
        let host = challenge.protectionSpace.host

        // Check if the current server type is not production
        if Constants.currentServerType != .production {
            // If not in production, bypass the certificate pinning check
            LogService.shared.logInfo("Bypassing certificate pinning check for non-production server.")
            completionHandler(.useCredential, URLCredential(trust: challenge.protectionSpace.serverTrust!))
            return
        }

        // Proceed with certificate pinning check for production server
        guard let serverTrust = challenge.protectionSpace.serverTrust else {
            LogService.shared.logCritical("No server trust provided")
            completionHandler(.cancelAuthenticationChallenge, nil)
            return
        }

        var error: CFError?
        guard SecTrustEvaluateWithError(serverTrust, &error) else {
            if let cfError = error {
                let nsError = cfError as Error as NSError
                LogService.shared.logCritical("SecTrustEvaluateWithError failed.", nsError)
            } else {
                LogService.shared.logCritical("SecTrustEvaluateWithError failed.")
            }
            completionHandler(.cancelAuthenticationChallenge, nil)
            return
        }

        guard let certificateChain = SecTrustCopyCertificateChain(serverTrust) as? [SecCertificate],
              let serverCertificate = certificateChain.first else {
            LogService.shared.logCritical("Failed to get server certificate from chain.")
            completionHandler(.cancelAuthenticationChallenge, nil)
            return
        }
        let serverCertificateData = SecCertificateCopyData(serverCertificate) as Data
        let decryptedBase64Certificate = Keys.Global().apiCert

        guard let localCertificateData = Data(base64Encoded: decryptedBase64Certificate) else {
            LogService.shared.logCritical("Failed to decode local certificate from base64.")
            completionHandler(.cancelAuthenticationChallenge, nil)
            return
        }

        if serverCertificateData == localCertificateData {
            LogService.shared.logInfo("Server certificate matches pinned certificate.")
            let credential = URLCredential(trust: serverTrust)
            completionHandler(.useCredential, credential)
            return
        } else {
            LogService.shared.logCritical("Server certificate does not match pinned certificate.")
            delegate?.didReceiveCertificateMismatchError() // Notify the delegate
        }
        completionHandler(.cancelAuthenticationChallenge, nil)
    }
tanderson-ld commented 1 week ago

Hi @houmie, thank you for posting. This repo is mainly intended for consumption in our other SDKs where certificate pinning is not required. We are always open to public contributions if you see a clean way to integrate pinning into the configuration. Another option is you fork the repo and perhaps tweak this region of the code.