Upgrade to Pro — share decks privately, control downloads, hide ads and more …

iOSアプリ内で不正なSSL証明書を検知する / SSL Pinning for iOS apps

iOSアプリ内で不正なSSL証明書を検知する / SSL Pinning for iOS apps

iOSDC Japan 2018
8/31 14:20~14:50 @Track A

Ask The Speakerでの質問と回答を追記しました

Keisuke Kobayashi

August 31, 2018
Tweet

More Decks by Keisuke Kobayashi

Other Decks in Programming

Transcript

  1. ࣗݾ঺հ • Keisuke Kobayashi • Twitter: kobakei122 • GitHub: kobakei

    • Kyash, Inc • Android / iOS / Engineering Manager
  2. தؒऀ߈ܸ • 2ऀͷ௨৴ͷؒʹ߈ܸऀ͕հࡏ͢Δ͜ͱͰɺ௨৴ͷ౪ ௌ΍վ͟ΜΛߦ͏߈ܸ • ӳޠͰ͸ Man in the middle

    (MITM) • ࠷΋Ұൠతͳͷ͸ɺෆਖ਼ͳΞΫηεϙΠϯτΛઃஔ͢ Δ͜ͱ • ྫɿո͍͠Free WiFiʹͭͳ͍ͩΒ৘ใ͕ൈ͔Εͨʂ
  3. ূ໌ॻ or ެ։伴ͷϐϯཹΊ • ূ໌ॻͷϐϯཹΊ • SSLαʔόʔূ໌ॻͦͷ΋ͷ͕Ұக͢Δ͔ݕূ • ߋ৽ස౓͕ߴ͍ =

    ΞϓϦΛසൟʹߋ৽ඞཁ • ެ։伴ͷϐϯཹΊ • ূ໌ॻͷݩʹͳͬͨެ։伴͕Ұக͢Δ͔ݕূ • ߋ৽ස౓͸௿͍
  4. SessionAdapter public protocol SessionAdapter { func createTask(with URLRequest: URLRequest, handler:

    @escaping (Data?, URLResponse?, Error?) -> Void) -> SessionTask func getTasks(with handler: @escaping ([SessionTask]) -> Void) }
  5. URLSessionAdapter open class URLSessionAdapter: NSObject, SessionAdapter, URLSessionDelegate, URLSessionTaskDelegate, URLSessionDataDelegate {

    open var urlSession: URLSession! public init(configuration: URLSessionConfiguration) { super.init() self.urlSession = URLSession(configuration: configuration, delegate: self, delegateQueue: nil) } open func createTask(with URLRequest: URLRequest, handler: @escaping (Data?, URLResponse?, Error?) -> Void) -> SessionTask { ... } open func getTasks(with handler: @escaping ([SessionTask]) -> Void) { ... } ... }
  6. URLSessionAdapter open class URLSessionAdapter: NSObject, SessionAdapter, URLSessionDelegate, URLSessionTaskDelegate, URLSessionDataDelegate {

    open var urlSession: URLSession! public init(configuration: URLSessionConfiguration) { super.init() self.urlSession = URLSession(configuration: configuration, delegate: self, delegateQueue: nil) } open func createTask(with URLRequest: URLRequest, handler: @escaping (Data?, URLResponse?, Error?) -> Void) -> SessionTask { ... } open func getTasks(with handler: @escaping ([SessionTask]) -> Void) { ... } ... }
  7. class MyURLSessionAdapter: URLSessionAdapter { func urlSession(_ session: URLSession, didReceive challenge:

    URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Swift.Void) { if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust { if let serverTrust = challenge.protectionSpace.serverTrust { var secresult = SecTrustResultType.invalid let status = SecTrustEvaluate(serverTrust, &secresult) if errSecSuccess == status { if let serverCertificate = SecTrustGetCertificateAtIndex(serverTrust, 0) { let serverCertificateData = SecCertificateCopyData(serverCertificate) let data = CFDataGetBytePtr(serverCertificateData); let size = CFDataGetLength(serverCertificateData); let cert1 = NSData(bytes: data, length: size) let fileDer = Bundle.main.path(forResource: "github", ofType: "der") if let file = fileDer { if let cert2 = NSData(contentsOfFile: file) { if cert1.isEqual(to: cert2 as Data) { completionHandler(URLSession.AuthChallengeDisposition.useCredential, URLCredential(trust:serverTrust)) return } } } } } } } // Pinning failed completionHandler(URLSession.AuthChallengeDisposition.cancelAuthenticationChallenge, nil) } }
  8. class MyURLSessionAdapter: URLSessionAdapter { func urlSession(_ session: URLSession, didReceive challenge:

    URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Swift.Void) { if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust { if let serverTrust = challenge.protectionSpace.serverTrust { var secresult = SecTrustResultType.invalid let status = SecTrustEvaluate(serverTrust, &secresult) if errSecSuccess == status { if let serverCertificate = SecTrustGetCertificateAtIndex(serverTrust, 0) { let serverCertificateData = SecCertificateCopyData(serverCertificate) let data = CFDataGetBytePtr(serverCertificateData); let size = CFDataGetLength(serverCertificateData); let cert1 = NSData(bytes: data, length: size) let fileDer = Bundle.main.path(forResource: "github", ofType: "der") if let file = fileDer { if let cert2 = NSData(contentsOfFile: file) { if cert1.isEqual(to: cert2 as Data) { completionHandler(URLSession.AuthChallengeDisposition.useCredential, URLCredential(trust:serverTrust)) return } } } } } } } // Pinning failed completionHandler(URLSession.AuthChallengeDisposition.cancelAuthenticationChallenge, nil) } }
  9. class MyURLSessionAdapter: URLSessionAdapter { func urlSession(_ session: URLSession, didReceive challenge:

    URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Swift.Void) { if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust { if let serverTrust = challenge.protectionSpace.serverTrust { var secresult = SecTrustResultType.invalid let status = SecTrustEvaluate(serverTrust, &secresult) if errSecSuccess == status { if let serverCertificate = SecTrustGetCertificateAtIndex(serverTrust, 0) { let serverCertificateData = SecCertificateCopyData(serverCertificate) let data = CFDataGetBytePtr(serverCertificateData); let size = CFDataGetLength(serverCertificateData); let cert1 = NSData(bytes: data, length: size) let fileDer = Bundle.main.path(forResource: "github", ofType: "der") if let file = fileDer { if let cert2 = NSData(contentsOfFile: file) { if cert1.isEqual(to: cert2 as Data) { completionHandler(URLSession.AuthChallengeDisposition.useCredential, URLCredential(trust:serverTrust)) return } } } } } } } // Pinning failed completionHandler(URLSession.AuthChallengeDisposition.cancelAuthenticationChallenge, nil) } } SSLαʔόʔূ໌ॻ
  10. class MyURLSessionAdapter: URLSessionAdapter { func urlSession(_ session: URLSession, didReceive challenge:

    URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Swift.Void) { if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust { if let serverTrust = challenge.protectionSpace.serverTrust { var secresult = SecTrustResultType.invalid let status = SecTrustEvaluate(serverTrust, &secresult) if errSecSuccess == status { if let serverCertificate = SecTrustGetCertificateAtIndex(serverTrust, 0) { let serverCertificateData = SecCertificateCopyData(serverCertificate) let data = CFDataGetBytePtr(serverCertificateData); let size = CFDataGetLength(serverCertificateData); let cert1 = NSData(bytes: data, length: size) let fileDer = Bundle.main.path(forResource: "github", ofType: "der") if let file = fileDer { if let cert2 = NSData(contentsOfFile: file) { if cert1.isEqual(to: cert2 as Data) { completionHandler(URLSession.AuthChallengeDisposition.useCredential, URLCredential(trust:serverTrust)) return } } } } } } } // Pinning failed completionHandler(URLSession.AuthChallengeDisposition.cancelAuthenticationChallenge, nil) } } αʔόʔূ໌ॻΛNSDataʹ
  11. class MyURLSessionAdapter: URLSessionAdapter { func urlSession(_ session: URLSession, didReceive challenge:

    URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Swift.Void) { if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust { if let serverTrust = challenge.protectionSpace.serverTrust { var secresult = SecTrustResultType.invalid let status = SecTrustEvaluate(serverTrust, &secresult) if errSecSuccess == status { if let serverCertificate = SecTrustGetCertificateAtIndex(serverTrust, 0) { let serverCertificateData = SecCertificateCopyData(serverCertificate) let data = CFDataGetBytePtr(serverCertificateData); let size = CFDataGetLength(serverCertificateData); let cert1 = NSData(bytes: data, length: size) let fileDer = Bundle.main.path(forResource: "github", ofType: "der") if let file = fileDer { if let cert2 = NSData(contentsOfFile: file) { if cert1.isEqual(to: cert2 as Data) { completionHandler(URLSession.AuthChallengeDisposition.useCredential, URLCredential(trust:serverTrust)) return } } } } } } } // Pinning failed completionHandler(URLSession.AuthChallengeDisposition.cancelAuthenticationChallenge, nil) } } ΫϥΠΞϯτͷDERϑΝΠϧ
  12. class MyURLSessionAdapter: URLSessionAdapter { func urlSession(_ session: URLSession, didReceive challenge:

    URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Swift.Void) { if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust { if let serverTrust = challenge.protectionSpace.serverTrust { var secresult = SecTrustResultType.invalid let status = SecTrustEvaluate(serverTrust, &secresult) if errSecSuccess == status { if let serverCertificate = SecTrustGetCertificateAtIndex(serverTrust, 0) { let serverCertificateData = SecCertificateCopyData(serverCertificate) let data = CFDataGetBytePtr(serverCertificateData); let size = CFDataGetLength(serverCertificateData); let cert1 = NSData(bytes: data, length: size) let fileDer = Bundle.main.path(forResource: "github", ofType: "der") if let file = fileDer { if let cert2 = NSData(contentsOfFile: file) { if cert1.isEqual(to: cert2 as Data) { completionHandler(URLSession.AuthChallengeDisposition.useCredential, URLCredential(trust:serverTrust)) return } } } } } } } // Pinning failed completionHandler(URLSession.AuthChallengeDisposition.cancelAuthenticationChallenge, nil) } } σʔλΛൺֱ
  13. class MyURLSessionAdapter: URLSessionAdapter { func urlSession(_ session: URLSession, didReceive challenge:

    URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Swift.Void) { if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust { if let serverTrust = challenge.protectionSpace.serverTrust { var secresult = SecTrustResultType.invalid let status = SecTrustEvaluate(serverTrust, &secresult) if errSecSuccess == status { if let serverCertificate = SecTrustGetCertificateAtIndex(serverTrust, 0) { let serverCertificateData = SecCertificateCopyData(serverCertificate) let data = CFDataGetBytePtr(serverCertificateData); let size = CFDataGetLength(serverCertificateData); let cert1 = NSData(bytes: data, length: size) let fileDer = Bundle.main.path(forResource: "github", ofType: "der") if let file = fileDer { if let cert2 = NSData(contentsOfFile: file) { if cert1.isEqual(to: cert2 as Data) { completionHandler(URLSession.AuthChallengeDisposition.useCredential, URLCredential(trust:serverTrust)) return } } } } } } } // Pinning failed completionHandler(URLSession.AuthChallengeDisposition.cancelAuthenticationChallenge, nil) } } ઀ଓࣦഊ
  14. let pinnedPublicKeyHash = "y2HhTRXXLdmAF1esYBb/muQUl3BIBdmEB8jUvMrGc28=" func urlSession(_ session: URLSession, didReceive challenge:

    URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Swift.Void) { if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust { if let serverTrust = challenge.protectionSpace.serverTrust { var secresult = SecTrustResultType.invalid let status = SecTrustEvaluate(serverTrust, &secresult) if errSecSuccess == status { if let serverCertificate = SecTrustGetCertificateAtIndex(serverTrust, 0) { let serverPublicKey = SecCertificateCopyPublicKey(serverCertificate) let serverPublicKeyData: NSData = SecKeyCopyExternalRepresentation(serverPublicKey!, nil )! let keyHash = sha256(data: serverPublicKeyData as Data) if (keyHash == pinnedPublicKeyHash) { completionHandler(.useCredential, URLCredential(trust:serverTrust)) return } } } } } // Pinning failed completionHandler(URLSession.AuthChallengeDisposition.cancelAuthenticationChallenge, nil) }
  15. let pinnedPublicKeyHash = "y2HhTRXXLdmAF1esYBb/muQUl3BIBdmEB8jUvMrGc28=" func urlSession(_ session: URLSession, didReceive challenge:

    URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Swift.Void) { if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust { if let serverTrust = challenge.protectionSpace.serverTrust { var secresult = SecTrustResultType.invalid let status = SecTrustEvaluate(serverTrust, &secresult) if errSecSuccess == status { if let serverCertificate = SecTrustGetCertificateAtIndex(serverTrust, 0) { let serverPublicKey = SecCertificateCopyPublicKey(serverCertificate) let serverPublicKeyData: NSData = SecKeyCopyExternalRepresentation(serverPublicKey!, nil )! let keyHash = sha256(data: serverPublicKeyData as Data) if (keyHash == pinnedPublicKeyHash) { completionHandler(.useCredential, URLCredential(trust:serverTrust)) return } } } } } // Pinning failed completionHandler(URLSession.AuthChallengeDisposition.cancelAuthenticationChallenge, nil) } ެ։伴ͷSHA256ϋογϡ
  16. ϋογϡग़ྗ #!/bin/bash certs=`openssl s_client -servername $1 -host $1 -port 443

    -showcerts </dev/null 2>/dev/null | sed -n '/ Certificate chain/,/Server certificate/p'` rest=$certs while [[ "$rest" =~ '-----BEGIN CERTIFICATE-----' ]] do cert="${rest%%-----END CERTIFICATE-----*}-----END CERTIFICATE-----" rest=${rest#*-----END CERTIFICATE-----} echo `echo "$cert" | grep 's:' | sed 's/.*s:\(.*\)/\1/'` echo "$cert" | openssl x509 -pubkey -noout | openssl rsa -pubin -outform der 2>/dev/null | openssl dgst -sha256 -binary | openssl enc -base64 done Ҿ༻: https://medium.com/@appmattus/android-security-ssl-pinning-1db8acb6621e
  17. let pinnedPublicKeyHash = "y2HhTRXXLdmAF1esYBb/muQUl3BIBdmEB8jUvMrGc28=" func urlSession(_ session: URLSession, didReceive challenge:

    URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Swift.Void) { if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust { if let serverTrust = challenge.protectionSpace.serverTrust { var secresult = SecTrustResultType.invalid let status = SecTrustEvaluate(serverTrust, &secresult) if errSecSuccess == status { if let serverCertificate = SecTrustGetCertificateAtIndex(serverTrust, 0) { let serverPublicKey = SecCertificateCopyPublicKey(serverCertificate) let serverPublicKeyData: NSData = SecKeyCopyExternalRepresentation(serverPublicKey!, nil )! let keyHash = sha256(data: serverPublicKeyData as Data) if (keyHash == pinnedPublicKeyHash) { completionHandler(.useCredential, URLCredential(trust:serverTrust)) return } } } } } // Pinning failed completionHandler(URLSession.AuthChallengeDisposition.cancelAuthenticationChallenge, nil) } αʔόʔূ໌ॻ
  18. let pinnedPublicKeyHash = "y2HhTRXXLdmAF1esYBb/muQUl3BIBdmEB8jUvMrGc28=" func urlSession(_ session: URLSession, didReceive challenge:

    URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Swift.Void) { if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust { if let serverTrust = challenge.protectionSpace.serverTrust { var secresult = SecTrustResultType.invalid let status = SecTrustEvaluate(serverTrust, &secresult) if errSecSuccess == status { if let serverCertificate = SecTrustGetCertificateAtIndex(serverTrust, 0) { let serverPublicKey = SecCertificateCopyPublicKey(serverCertificate) let serverPublicKeyData: NSData = SecKeyCopyExternalRepresentation(serverPublicKey!, nil )! let keyHash = sha256(data: serverPublicKeyData as Data) if (keyHash == pinnedPublicKeyHash) { completionHandler(.useCredential, URLCredential(trust:serverTrust)) return } } } } } // Pinning failed completionHandler(URLSession.AuthChallengeDisposition.cancelAuthenticationChallenge, nil) } αʔόʔূ໌ॻ͔Β ެ։伴ΛऔΓग़͢
  19. let pinnedPublicKeyHash = "y2HhTRXXLdmAF1esYBb/muQUl3BIBdmEB8jUvMrGc28=" func urlSession(_ session: URLSession, didReceive challenge:

    URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Swift.Void) { if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust { if let serverTrust = challenge.protectionSpace.serverTrust { var secresult = SecTrustResultType.invalid let status = SecTrustEvaluate(serverTrust, &secresult) if errSecSuccess == status { if let serverCertificate = SecTrustGetCertificateAtIndex(serverTrust, 0) { let serverPublicKey = SecCertificateCopyPublicKey(serverCertificate) let serverPublicKeyData: NSData = SecKeyCopyExternalRepresentation(serverPublicKey!, nil )! let keyHash = sha256(data: serverPublicKeyData as Data) if (keyHash == pinnedPublicKeyHash) { completionHandler(.useCredential, URLCredential(trust:serverTrust)) return } } } } } // Pinning failed completionHandler(URLSession.AuthChallengeDisposition.cancelAuthenticationChallenge, nil) } ެ։伴ͷSHA256ϋογϡ
  20. let pinnedPublicKeyHash = "y2HhTRXXLdmAF1esYBb/muQUl3BIBdmEB8jUvMrGc28=" func urlSession(_ session: URLSession, didReceive challenge:

    URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Swift.Void) { if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust { if let serverTrust = challenge.protectionSpace.serverTrust { var secresult = SecTrustResultType.invalid let status = SecTrustEvaluate(serverTrust, &secresult) if errSecSuccess == status { if let serverCertificate = SecTrustGetCertificateAtIndex(serverTrust, 0) { let serverPublicKey = SecCertificateCopyPublicKey(serverCertificate) let serverPublicKeyData: NSData = SecKeyCopyExternalRepresentation(serverPublicKey!, nil )! let keyHash = sha256(data: serverPublicKeyData as Data) if (keyHash == pinnedPublicKeyHash) { completionHandler(.useCredential, URLCredential(trust:serverTrust)) return } } } } } // Pinning failed completionHandler(URLSession.AuthChallengeDisposition.cancelAuthenticationChallenge, nil) } ެ։伴ϋογϡΛൺֱ
  21. ϦΫΤετ self.adapter = MyURLSessionAdapter(configuration: URLSessionConfiguration.default) self.session = Session(adapter: adapter) let

    request = RateLimitRequest() self.session.send(request) { result in switch result { case .success(let rateLimit): print("limit: \(rateLimit.limit)") print("remaining: \(rateLimit.remaining)") case .failure(let error): print("error: \(error)") } }
  22. ϦΫΤετ self.adapter = MyURLSessionAdapter(configuration: URLSessionConfiguration.default) self.session = Session(adapter: adapter) let

    request = RateLimitRequest() self.session.send(request) { result in switch result { case .success(let rateLimit): print("limit: \(rateLimit.limit)") print("remaining: \(rateLimit.remaining)") case .failure(let error): print("error: \(error)") } } MyURLSessionAdapter͔Β SessionΛ࡞੒
  23. ϦΫΤετ self.adapter = MyURLSessionAdapter(configuration: URLSessionConfiguration.default) self.session = Session(adapter: adapter) let

    request = RateLimitRequest() self.session.send(request) { result in switch result { case .success(let rateLimit): print("limit: \(rateLimit.limit)") print("remaining: \(rateLimit.remaining)") case .failure(let error): print("error: \(error)") } } SessionΠϯελϯεΛ࢖ͬͯ
 ϦΫΤετ
  24. SessionManager override func viewDidLoad() { super.viewDidLoad() // ϐϯཹΊ let serverTrustPolicies:

    [String: ServerTrustPolicy] = [ "api.github.com": .pinCertificates( certificates: ServerTrustPolicy.certificates(), validateCertificateChain: true, validateHost: true ), "insecure.expired-apis.com": .disableEvaluation ] self.sessionManager = SessionManager( serverTrustPolicyManager: ServerTrustPolicyManager(policies: serverTrustPolicies) ) }
  25. SessionManager override func viewDidLoad() { super.viewDidLoad() // ϐϯཹΊ let serverTrustPolicies:

    [String: ServerTrustPolicy] = [ "api.github.com": .pinCertificates( certificates: ServerTrustPolicy.certificates(), validateCertificateChain: true, validateHost: true ), "insecure.expired-apis.com": .disableEvaluation ] self.sessionManager = SessionManager( serverTrustPolicyManager: ServerTrustPolicyManager(policies: serverTrustPolicies) ) } ূ໌ॻϐϯཹΊ
  26. SessionManager override func viewDidLoad() { super.viewDidLoad() // ϐϯཹΊ let serverTrustPolicies:

    [String: ServerTrustPolicy] = [ "api.github.com": .pinPublicKeys( publicKeys: ServerTrustPolicy.publicKeys(), validateCertificateChain: true, validateHost: true ), "insecure.expired-apis.com": .disableEvaluation ] self.sessionManager = SessionManager( serverTrustPolicyManager: ServerTrustPolicyManager(policies: serverTrustPolicies) ) } ެ։伴ϐϯཹΊ
  27. SessionManager override func viewDidLoad() { super.viewDidLoad() // ϐϯཹΊ let serverTrustPolicies:

    [String: ServerTrustPolicy] = [ "api.github.com": .pinPublicKeys( publicKeys: ServerTrustPolicy.publicKeys(), validateCertificateChain: true, validateHost: true ), "insecure.expired-apis.com": .disableEvaluation ] self.sessionManager = SessionManager( serverTrustPolicyManager: ServerTrustPolicyManager(policies: serverTrustPolicies) ) } ϙϦγʔ͔Β SessionManagerΛ࡞੒
  28. ϦΫΤετ let url = "https://api.github.com/users/kobakei/repos" sessionManager.request(url, method: .get, parameters: nil,

    encoding: URLEncoding.default, headers: nil) .responseString { (response: DataResponse<String>) in // do something } SessionManagerΠϯελϯεΛ࢖ͬͯ ϦΫΤετ
  29. ࢀߟϦϯΫ • How to make your iOS apps more secure

    with SSL pinning • https://infinum.co/the-capsized-eight/how-to-make-your-ios-apps-more-secure-with-ssl-pinning • iOS certificate pinning with Swift and NSURLSession | StackOverflow • https://stackoverflow.com/questions/34223291/ios-certificate-pinning-with-swift-and-nsurlsession • Difference between Certificate pinning and public key pinning • https://security.stackexchange.com/questions/85209/difference-between-certificate-pinning-and-public-key- pinning • Why is SSL certificate pinning required? • https://stackoverflow.com/questions/45699036/why-is-ssl-certificate-pinning-required • iOSΞϓϦͷηΩϡΞίʔσΟϯάೖ໳ • https://www.jssec.org/dl/20160323_Ikuya_Fukumoto.pdf • LAC ηΩϡϦςΟ਍அϨϙʔτ