SSL이란

SSL(Secure Sockets Layer)은 암호화 기반 인터넷 보안 프로토콜입니다. 인터넷 통신의 개인정보 보호, 인증, 데이터 무결성을 보장하기 위해 Netscape가 1995년 처음으로 개발했습니다. SSL은 현재 사용 중인 TLS 암호화의 전신입니다.

 

TrustKit이란

TrustKit은 모든 iOS 10+, macOS 10.10+, tvOS 10+ 또는 watchOS 3+ 앱에서 SSL 공개 키 고정 및보고를 쉽게 배포 할 수있는 오픈 소스 프레임 워크입니다. Swift 및 Objective-C 앱을 모두 지원합니다.

수동으로 고정을 구성하는 것은 어렵고 시간이 많이 소요될 수 있습니다. TrusKit 은 코드 몇 줄만 작성 하여 인증서의 공개 키 를 확인 하는 매우 쉬운 방법 을 제공하고 추가 기능도 제공합니다.

https://github.com/datatheorem/TrustKit

https://github.com/datatheorem/TrustKit/blob/master/docs/getting-started.md

 

적용하기

1.인증서 다운로드

브라우저에서 사이트 인증서 다운로드 

 

2.인증서로부터 pin추출

https://github.com/datatheorem/TrustKit/blob/master/get_pin_from_certificate.py

python 소스 다운로드 후

 

Pin 추출 : U3xTWXJOd447ON2zOz9w35qNaPAJDpqlFO4Jt/443us= 

$ python3 get_pin_from_certificate.py --type DER \*.google.com.cer
CERTIFICATE INFO
----------------
b'subject= /C=US/ST=California/L=Mountain View/O=Google LLC/CN=*.google.com\nissuer= /C=US/O=Google Trust Services/CN=GTS CA 1O1\nSHA1 Fingerprint=E4:89:43:D9:6A:40:D5:34:B9:33:7E:E5:ED:A9:76:D2:20:1D:2E:BF\n'

TRUSTKIT CONFIGURATION
----------------------
kTSKPublicKeyHashes: @[@"b'U3xTWXJOd447ON2zOz9w35qNaPAJDpqlFO4Jt/443us='"] // You will also need to configure a backup pin

 

3.프로젝트에 TrustKit적용

pod 'TrustKit'
$ pod install

 

AppDelegate.swift

도메인 설정과 추출한 정보를 kTSKPublicKeyHashes값으로 설정

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        // Override point for customization after application launch.
        TrustKit.setLoggerBlock { (message) in
              print("TrustKit log: \(message)")
        }
        let trustKitConfig: [String: Any] = [
             kTSKSwizzleNetworkDelegates: false,
             kTSKPinnedDomains: [
                    "www.google.com": [
                           kTSKEnforcePinning: false,
                           kTSKIncludeSubdomains: true,
                           kTSKPublicKeyHashes: [
        //First public key -> Obtained from the Python script
        "U3xTWXJOd447ON2zOz9w35qNaPAJDpqlFO4Jt/443us=",
        //Second public key in case of the first one will expire
        "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB="
           ],
           kTSKReportUris:        ["https://overmind.datatheorem.com/trustkit/report"],
         ]
        ]]
        TrustKit.initSharedInstance(withConfiguration: trustKitConfig)
        
        return true
    }

 

HomeViewController.swift

 

delegate: self를 통해 재정의한 urlSession이 호출 되도록 선언함 

lazy var session: URLSession = {
       URLSession(configuration: URLSessionConfiguration.ephemeral,
                  delegate: self,
                  delegateQueue: OperationQueue.main)
    }()
let task = session.dataTask(with: url, completionHandler: { (data: Data?, response: URLResponse?, error: Error?) -> Void in
	guard let data = data else {
		completion(nil)
		return
	}
	let str = String(decoding: data, as: UTF8.self)
	print(str)
})
task.resume()
extension HomeViewController: URLSessionDelegate {
    
    func urlSession(_ session: URLSession,
                    didReceive challenge: URLAuthenticationChallenge,
                    completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
                                                
        if TrustKit.sharedInstance().pinningValidator.handle(challenge, completionHandler: completionHandler) == false {
            // TrustKit did not handle this challenge: perhaps it was not for server trust
            // or the domain was not pinned. Fall back to the default behavior
            completionHandler(.performDefaultHandling, nil)
        }
    }
}

URLSessionDelegate을 상속받아 urlSession에서 재정의하여 TrustKit을 통해 검증이 되도록 합니다. 

위와 같이 적용하여 정상적으로 동작하는 것을 확인합니다. 

서버에 https로 인증서를 적용하게되면 안드로이드에서 에러가 발생하게 됩니다.

   javax.net.ssl.SSLHandshakeException: java.security.cert.CertPathValidatorException: Trust anchor for certification path not found.
            at org.apache.harmony.xnet.provider.jsse.OpenSSLSocketImpl.startHandshake(OpenSSLSocketImpl.java:374)
            at libcore.net.http.HttpConnection.setupSecureSocket(HttpConnection.java:209)
            at libcore.net.http.HttpsURLConnectionImpl$HttpsEngine.makeSslConnection(HttpsURLConnectionImpl.java:478)
            at libcore.net.http.HttpsURLConnectionImpl$HttpsEngine.connect(HttpsURLConnectionImpl.java:433)
            at libcore.net.http.HttpEngine.sendSocketRequest(HttpEngine.java:290)
            at libcore.net.http.HttpEngine.sendRequest(HttpEngine.java:240)
            at libcore.net.http.HttpURLConnectionImpl.getResponse(HttpURLConnectionImpl.java:282)
            at libcore.net.http.HttpURLConnectionImpl.getInputStream(HttpURLConnectionImpl.java:177)
            at libcore.net.http.HttpsURLConnectionImpl.getInputStream(HttpsURLConnectionImpl.java:271)
    

 

정상적으로 접속하기 위해서 발급받은 인증서를 적용하도록 합니다.

자세한 내용은 https://developer.android.com/training/articles/security-ssl?hl=ko#kotlin 입니다.

 

코드 흐름은

  1. 인증서를 로드 (LetEncryption에서 fullchain.pem을 사용)

  2. 신뢰할 수있는 CA를 포함하는 키 스토어 생성

  3. CA 입력을 신뢰하는 TrustManager 생성

  4. TrustManager를 사용하는 SSLContext 생성

  5. OkHttpcliennt 적용

 

으로 전체 소스는 하기와 같습니다.

companion object {
        private const val BASE_URL = SERVER_API
        var VERIFY_DOMAIN: String = "*.lottois.info"

        fun create(context: Context): ApiService {
            val logger =
                HttpLoggingInterceptor().apply { level = HttpLoggingInterceptor.Level.BODY }

            val tmf: TrustManagerFactory? = getTrustManagerFactory(context)
            var sslsocket = tmf?.let { getSSLSocketFactory(it) }
            val hostnameVerifier = HostnameVerifier { _, session ->
                HttpsURLConnection.getDefaultHostnameVerifier().run {
                    verify(VERIFY_DOMAIN, session)
                }
            }

            val client = OkHttpClient.Builder()
                .addInterceptor(logger)
                .sslSocketFactory(sslsocket, tmf?.trustManagers?.get(0) as X509TrustManager)
                .hostnameVerifier(hostnameVerifier)
                .build()

            return Retrofit.Builder()
                .baseUrl(BASE_URL)
                .client(client)
                .addConverterFactory(GsonConverterFactory.create())
                .build()
                .create(ApiService::class.java)
        }

        private fun getTrustManagerFactory(context: Context): TrustManagerFactory? {
            // 1. CA 로드 
            val cf: CertificateFactory = CertificateFactory.getInstance("X.509")
            val caInput: InputStream = context.resources.openRawResource(R.raw.fullchain)
            val ca: X509Certificate = caInput.use {
                cf.generateCertificate(it) as X509Certificate
            }

            // 2. 신뢰할 수있는 CA를 포함하는 키 스토어 생성
            val keyStoreType = KeyStore.getDefaultType()
            val keyStore = KeyStore.getInstance(keyStoreType).apply {
                load(null, null)
                setCertificateEntry("ca", ca)
            }

            // 3. CA 입력을 신뢰하는 TrustManager 생성
            val tmfAlgorithm: String = TrustManagerFactory.getDefaultAlgorithm()
            val tmf: TrustManagerFactory = TrustManagerFactory.getInstance(tmfAlgorithm).apply {
                init(keyStore)
            }
            return tmf
        }

        private fun getSSLSocketFactory(
            tmf: TrustManagerFactory
        ): SSLSocketFactory? {
            //4. TrustManager를 사용하는 SSLContext 생성
            val sslContext: SSLContext = SSLContext.getInstance("TLS")
            sslContext.init(null, tmf.trustManagers, null)
            return sslContext.socketFactory
        }
    }

 

+ Recent posts