// // PortalTlsPinning.swift // CamExtension // // TLS Certificate Pinning for Camera Extension. // import Foundation import Security import CryptoKit import os.log public enum PortalTlsPinning { public static func extractLeafCert(from serverTrust: SecTrust) -> SecCertificate? { if #available(macOS 12.0, *) { if let chain = SecTrustCopyCertificateChain(serverTrust) as? [SecCertificate], !chain.isEmpty { return chain[0] } } let count = SecTrustGetCertificateCount(serverTrust) guard count > 0 else { return nil } return SecTrustGetCertificateAtIndex(serverTrust, 0) } public static func computeCertSha256(cert: SecCertificate) -> (data: Data, hex: String) { let certDer = SecCertificateCopyData(cert) as Data let digest = SHA256.hash(data: certDer) let data = Data(digest) let hex = data.map { String(format: "%02x", $0) }.joined() return (data, hex) } public static func evaluate( challenge: URLAuthenticationChallenge, pinnedFingerprint: String? = nil, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void ) { guard challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust, let serverTrust = challenge.protectionSpace.serverTrust else { completionHandler(.cancelAuthenticationChallenge, nil) return } guard let cert = extractLeafCert(from: serverTrust) else { os_log(.error, "PortalCam Extension TLS: No certificate found in server trust chain") completionHandler(.cancelAuthenticationChallenge, nil) return } let (_, certHashHex) = computeCertSha256(cert: cert) let targetFingerprint = pinnedFingerprint ?? PortalAuth.pinnedCertSha256 guard let pinned = targetFingerprint, !pinned.isEmpty else { os_log(.error, "PortalCam Extension TLS: No pinned certificate configured; rejecting connection") completionHandler(.cancelAuthenticationChallenge, nil) return } guard certHashHex.caseInsensitiveCompare(pinned) == .orderedSame else { os_log(.error, "PortalCam Extension TLS Pinning Mismatch! Expected: %{public}@, Got: %{public}@", pinned, certHashHex) completionHandler(.cancelAuthenticationChallenge, nil) return } completionHandler(.useCredential, URLCredential(trust: serverTrust)) } }