montana/iOS/Apps/Montana/Montana/Auth/CertificatePinning.swift

201 lines
7.1 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// CertificatePinning.swift
// Junona Montana Messenger
//
// Certificate Pinning для защиты от MITM атак
// "Доверяй только своим серверам"
//
import Foundation
import CommonCrypto
// MARK: - Montana Network Security
/// Защищённый URLSession с Certificate Pinning
/// Предотвращает MITM атаки, проверяя SHA256 хеш публичного ключа сервера
final class MontanaNetworkSecurity: NSObject, URLSessionDelegate {
static let shared = MontanaNetworkSecurity()
// MARK: - Pinned Public Key Hashes
/// SHA256 хеши публичных ключей серверов Montana
/// Формат: base64(SHA256(SubjectPublicKeyInfo))
///
/// Для получения хеша сертификата:
/// ```bash
/// openssl s_client -connect efir.org:443 </dev/null 2>/dev/null | \
/// openssl x509 -pubkey -noout | \
/// openssl pkey -pubin -outform DER | \
/// openssl dgst -sha256 -binary | base64
/// ```
private let pinnedHashes: Set<String> = [
// efir.org (primary)
// TODO: Заменить на реальный хеш после получения сертификата
"BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=",
// Let's Encrypt Root CA (backup)
"jQJTbIh0grw0/1TkHSumWb+Fs0Ggogr621gT3PvPKG0=",
// Let's Encrypt E1 Intermediate
"J2/oqMTsdhFWW/n85tys6b4yDBtb6idZayIEBx7QTxA="
]
/// Домены, для которых применяется pinning
private let pinnedDomains: Set<String> = [
"efir.org",
"176.124.208.93",
"72.56.102.240"
]
// MARK: - Secure Session
/// Защищённый URLSession с certificate pinning
lazy var secureSession: URLSession = {
let config = URLSessionConfiguration.default
config.timeoutIntervalForRequest = 30
config.timeoutIntervalForResource = 60
// Дополнительные настройки безопасности
config.tlsMinimumSupportedProtocolVersion = .TLSv12
config.httpShouldSetCookies = false
return URLSession(
configuration: config,
delegate: self,
delegateQueue: nil
)
}()
private override init() {
super.init()
}
// MARK: - URLSessionDelegate
func urlSession(
_ session: URLSession,
didReceive challenge: URLAuthenticationChallenge,
completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void
) {
// Проверяем только server trust challenges
guard challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust,
let serverTrust = challenge.protectionSpace.serverTrust else {
completionHandler(.performDefaultHandling, nil)
return
}
let host = challenge.protectionSpace.host
// Проверяем, нужен ли pinning для этого домена
guard shouldPinForHost(host) else {
// Для других доменов стандартная проверка
completionHandler(.performDefaultHandling, nil)
return
}
// Проверяем certificate chain
if validateCertificateChain(serverTrust: serverTrust, host: host) {
let credential = URLCredential(trust: serverTrust)
completionHandler(.useCredential, credential)
} else {
// Certificate pinning failed!
print("[SECURITY] ⚠️ Certificate pinning FAILED for \(host)")
print("[SECURITY] Possible MITM attack detected!")
completionHandler(.cancelAuthenticationChallenge, nil)
}
}
// MARK: - Pinning Logic
private func shouldPinForHost(_ host: String) -> Bool {
return pinnedDomains.contains(host)
}
private func validateCertificateChain(serverTrust: SecTrust, host: String) -> Bool {
// Стандартная проверка системы
var error: CFError?
guard SecTrustEvaluateWithError(serverTrust, &error) else {
print("[SECURITY] Trust evaluation failed: \(error?.localizedDescription ?? "unknown")")
return false
}
// Проверяем публичный ключ каждого сертификата в цепочке
let certificateCount = SecTrustGetCertificateCount(serverTrust)
for index in 0..<certificateCount {
guard let certificate = SecTrustGetCertificateAtIndex(serverTrust, index) else {
continue
}
// Получаем публичный ключ
guard let publicKey = SecCertificateCopyKey(certificate) else {
continue
}
// Получаем данные публичного ключа
var error: Unmanaged<CFError>?
guard let publicKeyData = SecKeyCopyExternalRepresentation(publicKey, &error) as Data? else {
continue
}
// Вычисляем SHA256 хеш
let hash = sha256(publicKeyData)
let hashBase64 = hash.base64EncodedString()
// Проверяем против pinned hashes
if pinnedHashes.contains(hashBase64) {
print("[SECURITY] ✅ Certificate pinning PASSED for \(host)")
return true
}
}
return false
}
private func sha256(_ data: Data) -> Data {
var hash = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH))
data.withUnsafeBytes {
_ = CC_SHA256($0.baseAddress, CC_LONG(data.count), &hash)
}
return Data(hash)
}
}
// MARK: - Convenience Extension
extension URLSession {
/// Защищённая сессия Montana с certificate pinning
static var montana: URLSession {
return MontanaNetworkSecurity.shared.secureSession
}
}
// MARK: - Debug Helper
#if DEBUG
extension MontanaNetworkSecurity {
/// Выводит информацию о сертификате для добавления в pinned hashes
func debugPrintCertificateInfo(for url: URL) {
Task {
var request = URLRequest(url: url)
request.httpMethod = "HEAD"
do {
let (_, response) = try await URLSession.shared.data(for: request)
if let httpResponse = response as? HTTPURLResponse {
print("[DEBUG] Response from \(url.host ?? "unknown"): \(httpResponse.statusCode)")
print("[DEBUG] To get certificate hash, run:")
print("openssl s_client -connect \(url.host ?? ""):\(url.port ?? 443) </dev/null 2>/dev/null | \\")
print(" openssl x509 -pubkey -noout | \\")
print(" openssl pkey -pubin -outform DER | \\")
print(" openssl dgst -sha256 -binary | base64")
}
} catch {
print("[DEBUG] Error: \(error)")
}
}
}
}
#endif