Ответ
Работа с самоподписанными сертификатами требует настройки URLSession для обработки SSL-рукопожатия в обход стандартной проверки доверенных центров сертификации (CA).
Основной подход — использование делегата URLSession:
- Реализовать метод
urlSession(_:didReceive:completionHandler:). - Принять (
useCredential) или отклонить (cancelAuthenticationChallenge) сертификат на основе кастомной логики.
Пример: Принятие конкретного самоподписанного сертификата из бандла
class SSLPinningDelegate: NSObject, URLSessionDelegate {
func urlSession(_ session: URLSession,
didReceive challenge: URLAuthenticationChallenge,
completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
guard challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust,
let serverTrust = challenge.protectionSpace.serverTrust else {
completionHandler(.performDefaultHandling, nil)
return
}
// 1. Загружаем локальный сертификат
guard let certPath = Bundle.main.path(forResource: "my_server", ofType: "der"),
let localCertData = try? Data(contentsOf: URL(fileURLWithPath: certPath)) else {
completionHandler(.cancelAuthenticationChallenge, nil)
return
}
// 2. Извлекаем удаленный сертификат
let remoteCert = SecTrustGetCertificateAtIndex(serverTrust, 0)
let remoteCertData = SecCertificateCopyData(remoteCert!) as Data
// 3. Сравниваем данные сертификатов (пиннинг)
if localCertData == remoteCertData {
let credential = URLCredential(trust: serverTrust)
completionHandler(.useCredential, credential)
} else {
completionHandler(.cancelAuthenticationChallenge, nil)
}
}
}
// Использование
let delegate = SSLPinningDelegate()
let session = URLSession(configuration: .default, delegate: delegate, delegateQueue: nil)
Ключевые моменты и предупреждения:
- ⚠️ Безопасность: Такой подход ослабляет защиту и должен использоваться только для тестирования (dev/staging) или внутри защищенных корпоративных сетей.
- Пиннинг (Pinning): Для продакшена рекомендуется пиннинг публичных ключей или сертификатов от доверенных CA, а не использование самоподписанных.
- Info.plist: Для iOS 9+ может потребоваться отключение ATS (
NSAppTransportSecurity) для доменов с такими сертификатами, что также является плохой практикой.
Ответ 18+ 🔞
Да ты посмотри, какая история! Сидишь ты такой, пишешь своё приложение, всё вроде красиво, а тут — бац! — сервер с самоподписанным сертификатом. И твой URLSession смотрит на это дело и такой: «Ну нахуй, я с таким не разговариваю». И всё, пиши пропало, запросы не летят.
А выход-то есть, да ещё какой! Надо просто научить свою сессию не быть такой занудой и доверять этому конкретному, пусть и самоподписанному, листочку бумаги. Делается это через делегата.
Слушай, как это выглядит в коде, сейчас обалдеешь. Берёшь и создаёшь класс, который будет этим всем безобразием руководить.
class SSLPinningDelegate: NSObject, URLSessionDelegate {
func urlSession(_ session: URLSession,
didReceive challenge: URLAuthenticationChallenge,
completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
// Сначала проверяем, а та ли это проблема? Может, сервер просто пароль просит?
guard challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust,
let serverTrust = challenge.protectionSpace.serverTrust else {
// Не наша тема, пусть система сама разбирается
completionHandler(.performDefaultHandling, nil)
return
}
// Ага, наша! Теперь достаём наш локальный сертификат, который мы как зеницу ока храним.
guard let certPath = Bundle.main.path(forResource: "my_server", ofType: "der"),
let localCertData = try? Data(contentsOf: URL(fileURLWithPath: certPath)) else {
// Не нашли? Ну всё, пиздец, обрываем связь.
completionHandler(.cancelAuthenticationChallenge, nil)
return
}
// Теперь вытаскиваем сертификат, который нам прислал сервер.
let remoteCert = SecTrustGetCertificateAtIndex(serverTrust, 0)
let remoteCertData = SecCertificateCopyData(remoteCert!) as Data
// И наконец-то, момент истины! Сравниваем два куска данных.
if localCertData == remoteCertData {
// Ура, они одинаковые! Это наш парень! Доверяем ему.
let credential = URLCredential(trust: serverTrust)
completionHandler(.useCredential, credential)
} else {
// Ой, а это кто такой? Не наш! Нахуй такого!
completionHandler(.cancelAuthenticationChallenge, nil)
}
}
}
Использовать — проще пареной репы:
let delegate = SSLPinningDelegate()
let session = URLSession(configuration: .default, delegate: delegate, delegateQueue: nil)
Всё, теперь твоя сессия будет общаться только с тем сервером, чей сертификат ты ей вручил, как пропуск.
НО! Э, погоди-ка, не спеши радоваться! Тут есть нюансы, ёпта, овердохуища!
- ⚠️ Безопасность — на нуле! Ты же понимаешь, что ты сейчас отключил встроенного охранника? Это как снять замок с двери и повесить записку «Входите, свои». Делай так ТОЛЬКО для тестов (dev/staging) или внутри своей закрытой сети, где все свои. Для прода — это пиздец какой риск.
- Правильный пиннинг — это не так. Для нормального, боевого приложения нужно цепляться не к самоподписанному сертификату, а к публичному ключу сертификата от нормального, доверенного центра сертификации (CA). Вот это будет мощно.
- Info.plist — тоже придётся трогать. На iOS 9 и выше эта система ATS такая бдительная, что может заблокировать соединение ещё до твоего делегата. Придётся в
Info.plistдля этого домена прописыватьNSExceptionAllowsInsecureHTTPLoads. И это тоже, блядь, красный флаг для ревьюеров и вообще для совести.