import Foundation import CommonCrypto import Security /// Pins the self-signed cert of the LAN deployment (CN=192.168.86.2, regenerated /// at container start — re-export the hash after server cert rotation, see README). /// Extract with: /// openssl s_client -connect 192.168.86.2:9178 /dev/null \ /// | openssl x509 -outform der | openssl dgst -sha256 enum TLSPinning { static let pinnedCertSHA256Hex: Set = [ "07907abde3bb9c685265909927627d9ea4705d7e7b27056eefe06e10b192af46" ] static let host = "192.168.86.2" static func session() -> URLSession { let config = URLSessionConfiguration.default config.timeoutIntervalForRequest = 15 return URLSession(configuration: config, delegate: PinnedTrustDelegate(), delegateQueue: nil) } static func sha256Hex(_ data: Data) -> String { var hash = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH)) data.withUnsafeBytes { buffer in guard let base = buffer.baseAddress else { return } CC_SHA256(base, CC_LONG(data.count), &hash) } return hash.map { String(format: "%02x", $0) }.joined() } static func matchesPinnedCert(_ trust: SecTrust) -> Bool { guard let chain = SecTrustCopyCertificateChain(trust) as? [SecCertificate], let leaf = chain.first, let der = SecCertificateCopyData(leaf) as Data? else { return false } return pinnedCertSHA256Hex.contains(sha256Hex(der)) } } final class PinnedTrustDelegate: NSObject, URLSessionDelegate { func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) { guard challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust, let trust = challenge.protectionSpace.serverTrust else { completionHandler(.performDefaultHandling, nil) return } // Apply the fixed self-signed pin only to the pinned LAN deployment // host. Other explicitly configured HTTPS hosts use normal system // trust evaluation (Finding 12). This allows public deployments and // other LAN hosts with valid certificates while never weakening trust // for the default self-signed host. let host = challenge.protectionSpace.host if host == TLSPinning.host { if TLSPinning.matchesPinnedCert(trust) { completionHandler(.useCredential, URLCredential(trust: trust)) } else { completionHandler(.cancelAuthenticationChallenge, nil) } } else { // Non-pinned host: let the system evaluate the certificate chain. completionHandler(.performDefaultHandling, nil) } } }