SSL пиннинг в flutter_inappwebview под iOS

Рейтинг: 1Ответов: 0Опубликовано: 17.01.2023

Пытаюсь встроить SSL спиннинг в пакет с pub.dev - flutter_inappwebview (сделал его локальным в проекте, чтобы можно было немного переписать). Есть показательный тестовый проект из 4-ёх файлов и самого SSL сертификата, но он под iOS.

Config.swift

import Foundation

enum Config {
    static func certificates(_ subdirectory: String?) -> [Data] {
        let certificates: [Data] = Bundle.main
            .urls(
                forResourcesWithExtension: "der",
                subdirectory: subdirectory
            )?.compactMap {
                do {
                    return try Data(contentsOf: $0)
                } catch let error {
                    assertionFailure("Не получается загрузить сертификат: \(error)")
                    return nil
                }
            } ?? []
        assert(!certificates.isEmpty, "Сертификаты не найдены")
        return certificates
    }
}

Сам файл сертификата в бандле.

Handling.swift


import Foundation

struct Handling<I, O> {
    var handle: (I) -> O

    init(handler: @escaping (I) -> O) {
        handle = handler
    }

    static func handle(_ handler: @escaping (I) -> O) -> Handling<I, O> { .init(handler: handler) }
}

extension Handling {
    static func chain(first: Handling, other handlers: [Handling],
                      passOverWhen passOver: @escaping (O) -> Bool) -> Handling {
        .handle {
            let firstHandlerResult = first.handle($0)
            guard passOver(firstHandlerResult) else { return firstHandlerResult }

            for handler in handlers {
                let handlingResult = handler.handle($0)
                guard passOver(handlingResult) else {
                    return handlingResult
                }
            }

            return firstHandlerResult
        }
    }
}

AuthChallenge.swift

import Foundation

typealias AuthChallengeResult = (URLSession.AuthChallengeDisposition, URLCredential?)
typealias AuthChallengeCompletion = (URLSession.AuthChallengeDisposition, URLCredential?) -> Void
typealias AuthChallengeHandler = Handling<URLAuthenticationChallenge, AuthChallengeResult>

typealias AsyncAuthChallengeHandler = Handling<(URLAuthenticationChallenge, AuthChallengeCompletion), Void>

extension AsyncAuthChallengeHandler {
    private static let handlerQueue = DispatchQueue(label: "WKWebView.AuthChallengeHandler")
    static func webViewAddTrusted(certificates: [Data],
                                  ignoreUserCertificates: Bool = true) -> AsyncAuthChallengeHandler {
        .handle { (challenge: URLAuthenticationChallenge, completion: @escaping AuthChallengeCompletion) in
            handlerQueue.async {
                guard ignoreUserCertificates else {
                    let result = AuthChallengeHandler.chain(
                        .setAnchor(certificates: certificates, includeSystemAnchors: true),
                        .secTrustEvaluateSSL(withCustomCerts: true)
                    ).handle(challenge)
                    completion(result.0, result.1)
                    return
                }
                _ = AuthChallengeHandler.setAnchor(
                    certificates: [], includeSystemAnchors: true
                ).handle(challenge)

                let systemCertsResult = AuthChallengeHandler.secTrustEvaluateSSL(
                    withCustomCerts: false
                ).handle(challenge)

                guard
                    systemCertsResult.0 != .performDefaultHandling,
                    systemCertsResult.0 != .useCredential
                else {
                    completion(systemCertsResult.0, systemCertsResult.1)
                    return
                }

                _ = AuthChallengeHandler.setAnchor(
                    certificates: certificates,
                    includeSystemAnchors: false
                ).handle(challenge)

                let customCertsResult = AuthChallengeHandler
                    .secTrustEvaluateSSL(withCustomCerts: true)
                    .handle(challenge)

                completion(customCertsResult.0, customCertsResult.1)
            }
        }
    }
}

extension AuthChallengeHandler {
    static func chain(_ nonEmpty: AuthChallengeHandler, _ handlers: AuthChallengeHandler...) -> AuthChallengeHandler {
        chain(first: nonEmpty, other: handlers, passOverWhen: { $0.0 == .performDefaultHandling })
    }
}

extension AuthChallengeHandler {
    static func setAnchor(certificates: [Data], includeSystemAnchors: Bool = false) -> Self {
        return .handle {
            guard let trust = serverTrust($0) else {
                return (.performDefaultHandling, nil)
            }
            SecTrustSetAnchorCertificates(
                trust, certificates.compactMap { SecCertificateCreateWithData(nil, $0 as CFData) } as CFArray
            )
            SecTrustSetAnchorCertificatesOnly(trust, !includeSystemAnchors)
            return (.performDefaultHandling, nil)
        }
    }
}

private extension AuthChallengeHandler {
    static func secTrustEvaluateSSL(withCustomCerts: Bool) -> AuthChallengeHandler {
        .handle {
            guard let trust = serverTrust($0) else {
                return (.performDefaultHandling, nil)
            }
            guard evaluate(trust, host: $0.protectionSpace.host, allowCustomRootCertificate: withCustomCerts) else {
                return (.cancelAuthenticationChallenge, nil)
            }
            return (.useCredential, URLCredential(trust: trust))
        }
    }

    static func serverTrust(_ authChallenge: URLAuthenticationChallenge) -> SecTrust? {
        guard authChallenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust else { return nil }
        return authChallenge.protectionSpace.serverTrust
    }

    static func evaluate(_ trust: SecTrust, host: String, allowCustomRootCertificate: Bool) -> Bool {
        let sslPolicy = SecPolicyCreateSSL(true, host as CFString)
        let status = SecTrustSetPolicies(trust, sslPolicy)
        if status != errSecSuccess {
            return false
        }

        var error: CFError?
        guard SecTrustEvaluateWithError(trust, &error) && error == nil else {
            return false
        }
        var result = SecTrustResultType.invalid
        let getTrustStatus = SecTrustGetTrustResult(trust, &result)
        guard getTrustStatus == errSecSuccess && (result == .unspecified || result == .proceed) else {
            return false
        }
        if allowCustomRootCertificate == false && result == .proceed { return false }
        return true
    }
}

ViewController.swift

import UIKit
import WebKit

class ViewController: UIViewController, WKNavigationDelegate {
    var webView: WKWebView!

    override func viewDidLoad() {
        super.viewDidLoad()
        settingLayout()

        webView.navigationDelegate = self

        let sber = "https://sberbank.com/ru/certificates"
        let url = URL(string: sber)!
        webView.load(URLRequest(url: url))
        webView.allowsBackForwardNavigationGestures = true
    }

    func settingLayout() {
        webView = WKWebView()
        webView.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(webView)

        NSLayoutConstraint.activate([
            webView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
            webView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor),
            webView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
            webView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
        ])
    }
    func webView(_ webView: WKWebView, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
        let data = Config.certificates("Certificates")
        AsyncAuthChallengeHandler.webViewAddTrusted(certificates: data).handle((challenge, completionHandler))
    }
}

Из него я пытаюсь перенести сам механизм SSL пиннинга в flutter_inappwebview. У flutter_inappwebview в файле InAppWebView.swift есть такой же метод:

public func webView(_ webView: WKWebView, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
        
        if windowId != nil, !windowCreated {
            completionHandler(.cancelAuthenticationChallenge, nil)
            return
        }
        
        if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodHTTPBasic ||
            challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodDefault ||
            challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodHTTPDigest ||
            challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodNegotiate ||
            challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodNTLM {
            let host = challenge.protectionSpace.host
            let prot = challenge.protectionSpace.protocol
            let realm = challenge.protectionSpace.realm
            let port = challenge.protectionSpace.port
            onReceivedHttpAuthRequest(challenge: challenge, result: {(result) -> Void in
                if result is FlutterError {
                    print((result as! FlutterError).message ?? "")
                    completionHandler(.performDefaultHandling, nil)
                }
                else if (result as? NSObject) == FlutterMethodNotImplemented {
                    completionHandler(.performDefaultHandling, nil)
                }
                else {
                    var response: [String: Any]
                    if let r = result {
                        response = r as! [String: Any]
                        var action = response["action"] as? Int
                        action = action != nil ? action : 0;
                        switch action {
                            case 0:
                                InAppWebView.credentialsProposed = []
                                // used .performDefaultHandling to mantain consistency with Android
                                // because .cancelAuthenticationChallenge will call webView(_:didFail:withError:)
                                completionHandler(.performDefaultHandling, nil)
                                //completionHandler(.cancelAuthenticationChallenge, nil)
                                break
                            case 1:
                                let username = response["username"] as! String
                                let password = response["password"] as! String
                                let permanentPersistence = response["permanentPersistence"] as? Bool ?? false
                                let persistence = (permanentPersistence) ? URLCredential.Persistence.permanent : URLCredential.Persistence.forSession
                                let credential = URLCredential(user: username, password: password, persistence: persistence)
                                completionHandler(.useCredential, credential)
                                break
                            case 2:
                                if InAppWebView.credentialsProposed.count == 0, let credentialStore = CredentialDatabase.credentialStore {
                                    for (protectionSpace, credentials) in credentialStore.allCredentials {
                                        if protectionSpace.host == host && protectionSpace.realm == realm &&
                                        protectionSpace.protocol == prot && protectionSpace.port == port {
                                            for credential in credentials {
                                                InAppWebView.credentialsProposed.append(credential.value)
                                            }
                                            break
                                        }
                                    }
                                }
                                if InAppWebView.credentialsProposed.count == 0, let credential = challenge.proposedCredential {
                                    InAppWebView.credentialsProposed.append(credential)
                                }
                                
                                if let credential = InAppWebView.credentialsProposed.popLast() {
                                    completionHandler(.useCredential, credential)
                                }
                                else {
                                    completionHandler(.performDefaultHandling, nil)
                                }
                                break
                            default:
                                InAppWebView.credentialsProposed = []
                                completionHandler(.performDefaultHandling, nil)
                        }
                        return;
                    }
                    completionHandler(.performDefaultHandling, nil)
                }
            })
        }
        else if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust {

            guard let serverTrust = challenge.protectionSpace.serverTrust else {
                completionHandler(.performDefaultHandling, nil)
                return
            }

            onReceivedServerTrustAuthRequest(challenge: challenge, result: {(result) -> Void in
                if result is FlutterError {
                    print((result as! FlutterError).message ?? "")
                    completionHandler(.performDefaultHandling, nil)
                }
                else if (result as? NSObject) == FlutterMethodNotImplemented {
                    completionHandler(.performDefaultHandling, nil)
                }
                else {
                    var response: [String: Any]
                    if let r = result {
                        response = r as! [String: Any]
                        var action = response["action"] as? Int
                        action = action != nil ? action : 0;
                        switch action {
                            case 0:
                                InAppWebView.credentialsProposed = []
                                completionHandler(.cancelAuthenticationChallenge, nil)
                                break
                            case 1:
                                let exceptions = SecTrustCopyExceptions(serverTrust)
                                SecTrustSetExceptions(serverTrust, exceptions)
                                let credential = URLCredential(trust: serverTrust)
                                completionHandler(.useCredential, credential)
                                break
                            default:
                                InAppWebView.credentialsProposed = []
                                completionHandler(.performDefaultHandling, nil)
                        }
                        return;
                    }
                    completionHandler(.performDefaultHandling, nil)
                }
            })
        }
        else if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodClientCertificate {
            onReceivedClientCertRequest(challenge: challenge, result: {(result) -> Void in
                if result is FlutterError {
                    print((result as! FlutterError).message ?? "")
                    completionHandler(.performDefaultHandling, nil)
                }
                else if (result as? NSObject) == FlutterMethodNotImplemented {
                    completionHandler(.performDefaultHandling, nil)
                }
                else {
                    var response: [String: Any]
                    if let r = result {
                        response = r as! [String: Any]
                        var action = response["action"] as? Int
                        action = action != nil ? action : 0;
                        switch action {
                            case 0:
                                completionHandler(.cancelAuthenticationChallenge, nil)
                                break
                            case 1:
                                let certificatePath = response["certificatePath"] as! String;
                                let certificatePassword = response["certificatePassword"] as? String ?? "";
                                
                                do {
                                    let path = try Util.getAbsPathAsset(assetFilePath: certificatePath)
                                    let PKCS12Data = NSData(contentsOfFile: path)!
                                    
                                    if let identityAndTrust: IdentityAndTrust = self.extractIdentity(PKCS12Data: PKCS12Data, password: certificatePassword) {
                                        let urlCredential: URLCredential = URLCredential(
                                            identity: identityAndTrust.identityRef,
                                            certificates: identityAndTrust.certArray as? [AnyObject],
                                            persistence: URLCredential.Persistence.forSession);
                                        completionHandler(.useCredential, urlCredential)
                                    } else {
                                        completionHandler(.performDefaultHandling, nil)
                                    }
                                } catch {
                                    print(error.localizedDescription)
                                    completionHandler(.performDefaultHandling, nil)
                                }
                                
                                break
                            case 2:
                                completionHandler(.cancelAuthenticationChallenge, nil)
                                break
                            default:
                                completionHandler(.performDefaultHandling, nil)
                        }
                        return;
                    }
                    completionHandler(.performDefaultHandling, nil)
                }
            })
        }
        else {
            completionHandler(.performDefaultHandling, nil)
        }
    }

Я перенёс Config.swift, Handling.swift и AuthChallenge.swift полностью во Flutter проект, а в метод webView добавляю дополнительно код из ViewController.swift :

    public func webView(_ webView: WKWebView, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
  
        // Мой код
            let data = Config.certificates("Certificates")

// На этой строке проект выдаёт ошибку:
        AsyncAuthChallengeHandler.webViewAddTrusted(certificates: data).handle((challenge, completionHandler)) 
        // Мой код

        if windowId != nil, !windowCreated {
            completionHandler(.cancelAuthenticationChallenge, nil)
            return
        }

Полный исходный код iOS проекта: https://securepayments.sberbank.ru/wiki/lib/exe/fetch.php/certificates:add:wkwebview.zip

Исходный код flutter_inappwebview : https://github.com/pichillilorenzo/flutter_inappwebview/tree/master/ios/Classes/InAppWebView

По итогу ошибка :

libsystem_kernel.dylib`:
    0x1b054a0c4 <+0>:  mov    x16, #0x148
    0x1b054a0c8 <+4>:  svc    #0x80
->  0x1b054a0cc <+8>:  b.lo   0x1b054a0e8               ; <+36>
    0x1b054a0d0 <+12>: stp    x29, x30, [sp, #-0x10]!
    0x1b054a0d4 <+16>: mov    x29, sp
    0x1b054a0d8 <+20>: bl     0x1b0542c28               ; cerror_nocancel
    0x1b054a0dc <+24>: mov    sp, x29
    0x1b054a0e0 <+28>: ldp    x29, x30, [sp], #0x10
    0x1b054a0e4 <+32>: ret    
    0x1b054a0e8 <+36>: ret
Thread 6: "* -[NSProxy doesNotRecognizeSelector:protectionSpace] called!"
2023-01-17 16:02:21.796830+0300 Runner[5639:126878] [SceneConfiguration] Info.plist contained no UIScene configuration dictionary (looking for configuration named "(no name)")
2023-01-17 16:02:21.962879+0300 Runner[5639:126878] Metal API Validation Enabled
2023-01-17 16:02:22.139889+0300 Runner[5639:127926] flutter: The Dart VM service is listening on http://127.0.0.1:50392/lP6xF-3LIqQ=/
Сертификаты загружены
2023-01-17 16:02:23.685094+0300 Runner[5639:126878] [Security] This method should not be called on the main thread as it may lead to UI unresponsiveness.
2023-01-17 16:02:23.688352+0300 Runner[5639:127886] * Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '* -[NSProxy doesNotRecognizeSelector:protectionSpace] called!'
* First throw call stack:
(
    0   CoreFoundation                      0x000000018040e7c8 __exceptionPreprocess + 172
    1   libobjc.A.dylib                     0x0000000180051144 objc_exception_throw + 56
    2   Foundation                          0x0000000180bcb0d0 +[NSProxy instanceMethodSignatureForSelector:] + 0
    3   CoreFoundation                      0x00000001804126c8 __forwarding__ + 1308
    4   CoreFoundation                      0x0000000180414b4c _CF_forwarding_prep_0 + 92
    5   flutter_inappwebview                0x0000000100cd58fc $s20flutter_inappwebview8HandlingVAASo28NSURLAuthenticationChallengeCRszSo016NSURLSessionAuthE11DispositionV_So15NSURLCredentialCSgtRs_rlE11serverTrust33_0684D36404BCE6E953B6A9229C981837LLySo03SecK3RefaSgAEFZ + 40
    6   flutter_inappwebview                0x0000000100cd5670 $s20flutter_inappwebview8HandlingVAASo28NSURLAuthenticationChallengeCRszSo016NSURLSessionAuthE11DispositionV_So15NSURLCredentialCSgtRs_rlE9setAnchor12certificates20includeSystemAnchorsACyAeG_AJtGSay10Foundation4DataVG_SbtFZAG_AJtAEcfU_ + 84
    7   flutter_inappwebview                0x0000000100cd4d68 $s20flutter_inappwebview8HandlingVAASo28NSURLAuthenticationChallengeC_ySo016NSURLSessionAuthE11DispositionV_So15NSURLCredentialCSgtctRszytRs_rlE17webViewAddTrusted12certificates22ignoreUserCertificatesACyAE_yAG_AJtctytGSay10Foundation4DataVG_SbtFZyAE_yAG_AJtctcfU_yycfU_ + 212
    8   flutter_inappwebview                0x0000000100cd5324 $sIeg_IeyB_TR + 48
    9   libdispatch.dylib                   0x0000000100ad0594 _dispatch_call_block_and_release + 24
    10  libdispatch.dylib                   0x0000000100ad1d5c _dispatch_client_callout + 16
    11  libdispatch.dylib                   0x0000000100ada040 _dispatch_lane_serial_drain + 928
    12  libdispatch.dylib                   0x0000000100adad80 _dispatch_lane_invoke + 428
    13  libdispatch.dylib                   0x0000000100ae8b40 _dispatch_workloop_worker_thread + 1720
    14  libsystem_pthread.dylib             0x00000001b059a8fc _pthread_wqthread + 284
    15  libsystem_pthread.dylib             0x00000001b05996c0 start_wqthread + 8
)
libc++abi: terminating with uncaught exception of type NSException
* Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '* -[NSProxy doesNotRecognizeSelector:protectionSpace] called!'
terminating with uncaught exception of type NSException
CoreSimulator 857.14 - Device: iPhone 12 Pro Max (21F5C6F0-7A32-49C9-AD57-A0B53AAA2E84) - Runtime: iOS 16.2 (20C52) - DeviceType: iPhone 12 Pro Max

Подскажите пожалуйста, что я упускаю или возможно где-то допускаю грубейшие ошибки, зарание извиняюсь, так как не высокий уровень владения Swift и iOS. Если кто-то имел схожий опыт, подскажите, возможно существует более удобный способ SSL пиннинга в flutter_inappwebview конкретно для iOS?

Ответы

Ответов пока нет.