SSL пиннинг в flutter_inappwebview под iOS
Пытаюсь встроить 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?