Как подключить российский SSL-сертификат к iOS-приложению

1708aaefd37cbf66a4895c9550a84a5e.png

Одна из санкций, которая досталась России, — запрет на выдачу и продление SSL-сертификатов. Это приводит к тому, что у некоторых компаний сертификат может протухнуть и сайты перестанут открываться.

Основных решений два:

  1. Использовать российский Яндекс.Браузер или Атом.

  2. Поставить на компьютер сертификат или профиль от Минцифры.

Для мобильных приложений это превращается в особую проблему — могут перестать проходить платежи разных эквайрингов.

Например,  15 февраля 2023 года у Сбера истечёт действие сертификата и надо переходить на самоподписанный. Если этого не сделать, то эквайринг через Сбер может перестать работать. SberPay будет работать как и раньше.

В статье покажу, что делать разработчикам приложений, чтобы экраны c 3-D Secure открывались и эквайринг продолжал работу.

Info.plist

На уровне конфигурации проекта ничего менять не нужно. Скорее всего, у вас уже стоит флаг NSAllowsArbitraryLoadsInWebContent в Info.plist: он нужен, чтобы вообще уметь хоть что-то грузить в WKWebView.

NSAppTransportSecurity

  NSAllowsArbitraryLoadsInWebContent
  

На ревью Apple спросит, зачем это вам — расскажите про 3-D Secure.

Подставляем правильный сертификат

Сертификаты берём отсюда https://www.gosuslugi.ru/crt. Нам понадобится сертификат для macOS в .pem формате.

Увы, это страница для пользователей, а не для разработчиков, поэтому сертификаты придётся привести в другой вид. Важно, что сертификатов два: Russian Trusted Root CA и Russian Trusted Sub CA. Нужны оба.

Чтобы получить их, можно взять .pem файл, разделить его на два и сконвертировать каждый в .der, потому что iOS только с .der умеет работать.

openssl x509 -outform der -in certificate1.pem -out certificate1.der

Если вы почему-то доверяете мне, а не Минцифре, то можете сразу взять готовые:

Russian Trusted Sub CA.der

Russian Trusted Root CA.der

Добавляйте их в проект, линкуйте так, чтобы они попал в бандл.

Дополнительная проверка сертификата

Если экран не проходит стандартную валидацию сертификатом, зашитым в операционную систему, то в делегате WKWebView вызывается дополнительная проверка на проверку подключения. По документации мы должны спросить у пользователя, стоит ли открывать эту страницу или как-то иначе проверить безопасность подключения. Будем разрешать подключение после проверки сертификата от Минцифры.

Поймаем провалившуюся проверку. В челлендже есть serverTrust-объект, по которому мы должны принять решение. Если валидация не прошла, то возвращаемся к дефолтному поведению через .performDefaultHandling

extension ViewController: WKNavigationDelegate {
    func webView(
        _ webView: WKWebView,
        didReceive challenge: URLAuthenticationChallenge,
        completionHandler: @escaping (URLSession.AuthChallengeDisposition, 
                                      URLCredential?) -> Void
    ) {
        guard let serverTrust = challenge.protectionSpace.serverTrust 
        else { return completionHandler(.performDefaultHandling, nil) }
        
        Task.detached(priority: .userInitiated) {
            if await self.validator.checkValidity(of: serverTrust) {
                // Allow our sertificate
                let cred = URLCredential(trust: serverTrust)
                completionHandler(.useCredential, cred)
            } else {
                // Default check for another connections
                completionHandler(.performDefaultHandling, nil)
            }
        }
    }
}

Всю работу с сертификатами спрячем в валидатор. Вся работа должна быть в отдельном потоке, поэтому весь код обёрнут в актор для синхронизации значений внутри него. В примере много специфичного кода из фреймворка Security, но его документация все объясняет очень подробно.

actor CertificateValidator {
    var certificates = [SecCertificate]()
   
    func prepareCertificates(_ names: [String]) {
        certificates = names.compactMap(certificate(name:))
    }

    private func certificate(name: String) -> SecCertificate? {
        let path = Bundle.main.url(forResource: name, withExtension: "der")
        let certData = try! Data(contentsOf: path!)
        
        let certificate = SecCertificateCreateWithData(nil, certData as CFData)
        return certificate
    }
  
    func checkValidity(of serverTrust: SecTrust, anchorCertificatesOnly: Bool = false) -> Bool {
        SecTrustSetAnchorCertificates(serverTrust, certificates as CFArray)
        SecTrustSetAnchorCertificatesOnly(serverTrust, anchorCertificatesOnly)

        var error: CFError?
        let isTrusted = SecTrustEvaluateWithError(serverTrust, &error)
        
        return isTrusted
    }
}

Ну и вызовем загрузку сертификатов во viewDidLoad:

class ViewController: UIViewController {

    let validator = CertificateValidator()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        Task {
            let names = ["Russian Trusted Root CA",
                         "Russian Trusted Sub CA"]
            await validator.prepareCertificates(names)
        }

        
        let url = URL(string: "https://3dsecmt.sberbank.ru/payment/se/keys.do")!
        webView.navigationDelegate = self  
        webView.load(URLRequest(url: url))
    }

    @IBOutlet weak var webView: WKWebView!
}

Полный пример вы можете посмотреть в GitHub. Запускайте проект, сайт должен открыться.

В статье я показал самый простой пример, чтобы была понятна суть, но в руководстве от Сбера вы можете найти полный текст с дополнительными проверками.g

Вопросы

Как это сделать на Andriod?

Посмотрите инструкцию от Сбера

Будут ли проблемы у других банков?

Рано или поздно. Решение прогоняет все запросы через сертификат Минцифры, поэтому должно быть массовым решением.

Что будет с эквайрингами в других странах?

Если мы не прошли проверку по сертификату Минцифры, то передаём контроль поведению по умолчанию, что позволит грузиться и иностранным эквайрингам.

Как это решение влияет на PCI DSS?

Никак не должно влиять, потому что NSAllowsArbitraryLoadsInWebContent и так стоял, а других решений для России нет пока.

Когда надо обновлять сертификат?

Через 5 лет истекает самый короткоживущий. Заведите себе напоминание обновить за год, тогда большинство пользователей бесшовно обновится.

Сертификат Минцифры безопасен?

По сути, вы передаёте возможность читать ваш трафик государству. Подходит вам это или нет — решайте сами, у всех разные ситуации. Можно отдать это на откуп пользователям, чтобы они сами поставили нужный профиль, но большинство из них просто не поймут проблему и способ решения, так как столкнутся с этим впервые.

Вижу в логах текст [Security] This method should not be called on the main thread as it may lead to UI unresponsiveness.

Вся работа с фреймворком Security должна быть в фоновом потоке, но видимо где-то в WKWebView это нарушено.

У меня эквайринг подключен через SDK и я не могу поменять код, что делать?

Скорее всего последняя версия SDK уже содержит это исправление. Уточните у своего экваера.

Надеюсь, статья поможет вовремя выпустить обновления для приложений и не потерять в деньгах. Если у вас есть вопросы или вы можете дополнить тему — приходите в комментарии.

Если понравилась статья и хочешь больше узнать о разработке приложений Dodo Brands, подписывайся на наш канал Dodo Mobile.

© Habrahabr.ru