Одна из санкций, которая досталась России, — запрет на выдачу и продление SSL-сертификатов. Это приводит к тому, что у некоторых компаний сертификат может протухнуть и сайты перестанут открываться.
Основных решений два:
Использовать российский Яндекс.Браузер или Атом.
Поставить на компьютер сертификат или профиль от Минцифры.
Для мобильных приложений это превращается в особую проблему — могут перестать проходить платежи разных эквайрингов.
Например, 15 февраля 2023 года у Сбера истечёт действие сертификата и надо переходить на самоподписанный. Если этого не сделать, то эквайринг через Сбер может перестать работать. SberPay будет работать как и раньше.
В статье покажу, что делать разработчикам приложений, чтобы экраны c 3-D Secure открывались и эквайринг продолжал работу.
Info.plist
На уровне конфигурации проекта ничего менять не нужно. Скорее всего, у вас уже стоит флаг NSAllowsArbitraryLoadsInWebContent
в Info.plist
: он нужен, чтобы вообще уметь хоть что-то грузить в WKWebView
.
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoadsInWebContent</key>
<true/>
</dict>
На ревью 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
Если вы почему-то доверяете мне, а не Минцифре, то можете сразу взять готовые:
Добавляйте их в проект, линкуйте так, чтобы они попал в бандл.
Дополнительная проверка сертификата
Если экран не проходит стандартную валидацию сертификатом, зашитым в операционную систему, то в делегате 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 лет истекает самый короткоживущий. Заведите себе напоминание обновить за год, тогда большинство пользователей бесшовно обновится. Мы себе написали unit-тест который упадет за год до истечения срока сертификата.
Сертификат Минцифры безопасен?
По сути, вы передаёте возможность читать ваш трафик государству. Подходит вам это или нет — решайте сами, у всех разные ситуации. Можно отдать это на откуп пользователям, чтобы они сами поставили нужный профиль, но большинство из них просто не поймут проблему и способ решения, так как столкнутся с этим впервые.
Вижу в логах текст [Security] This method should not be called on the main thread as it may lead to UI unresponsiveness.
Вся работа с фреймворком Security
должна быть в фоновом потоке, но видимо где-то в WKWebView
это нарушено.
У меня эквайринг подключен через SDK и я не могу поменять код, что делать?
Скорее всего последняя версия SDK уже содержит это исправление. Уточните у своего экваера.
Есть ли решение для UIWebView?
Я такого не нашел
Надеюсь, статья поможет вовремя выпустить обновления для приложений и не потерять в деньгах. Если у вас есть вопросы или вы можете дополнить тему — приходите в комментарии.
Если понравилась статья и хочешь больше узнать о разработке приложений Dodo Brands, подписывайся на наш канал Dodo Mobile.