본문 바로가기
Android

[Android/Kotlin] Retrofit을 이용한 https통신에서 SSL 인증서 오류 해결방법

by JongSeok 2024. 1. 30.

문제상황

안드로이드에서는 버전 9부터 네트워크 통신 과정에서 http 접근을 허용하고 있지 않습니다.

하지만 Manifest에 usesCleartextTraffic 속성을 true로 설정해주는 것만으로도 우회해서 http 접근이 가능하기 때문에 그동안 https://xxxx로 적용된 API 주소를 단순히 http://xxxx로 바꿔 요청하는 방식으로 개발했습니다.

하지만 이번 프로젝트에서는 서버에서 어떤 보안(?) 처리가 되어있는지 http 접근이 도저히 되지 않았습니다.

 

javax.net.ssl.SSLHandshakeException: java.security.cert.CertPathValidatorException: Trust anchor for certification path not found.

Retrofit 통신 error 로그

원인을 찾아보니 요청 주소에서 요구하는 SSL 인증서를 다운받아 OkHttp 객체에 적용시켜줘야 했습니다.

SSL 인증서를 적용해보자.


적용

저희 팀의 서버 주소는 공개할 수 없지만 API를 요청하는 도메인 주소에 접속해 검색바 좌측 '사이트 정보 보기'에서 인증서를 다운받을 수 있습니다.

res → raw

다운받은 인증서는 프로젝트의 res폴더 raw패키지를 만들어 .crt 확장자로 저장합니다.

 

-----BEGIN CERTIFICATE-----
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
...
-----END CERTIFICATE-----

인증서는 위와 같은 양식입니다.

 

그리고 인증서 적용을 도와줄 SelfSigningHelper 클래스를 정의합니다.

(저는 Hilt를 사용해 DI를 적용하고 있는 부분 참고 바랍니다.)

SelfSigningHelper.kt

@Singleton
class SelfSigningHelper @Inject constructor(
    @ApplicationContext context: Context
) {
    lateinit var tmf: TrustManagerFactory
    lateinit var sslContext: SSLContext

    init {
        val cf: CertificateFactory
        val ca: Certificate

        val caInput: InputStream

        try {
            cf = CertificateFactory.getInstance("X.509")

            caInput = context.resources.openRawResource(R.raw.tellingme_store)

            ca = cf.generateCertificate(caInput)
            println("ca = ${(ca as X509Certificate).subjectDN}")

            // Create a KeyStore containing our trusted CAs
            val keyStoreType = KeyStore.getDefaultType()
            val keyStore = KeyStore.getInstance(keyStoreType)
            with(keyStore) {
                load(null, null)
                keyStore.setCertificateEntry("ca", ca)
            }

            // Create a TrustManager that trusts the CAs in our KeyStore
            val tmfAlgorithm = TrustManagerFactory.getDefaultAlgorithm()
            tmf = TrustManagerFactory.getInstance(tmfAlgorithm)
            tmf.init(keyStore)

            // Create an SSLContext that uses our TrustManager
            sslContext = SSLContext.getInstance("TLS")
            sslContext.init(null, tmf.trustManagers, java.security.SecureRandom())

            caInput.close()
        } catch (e: KeyStoreException) {
            e.printStackTrace()
        } catch (e: CertificateException) {
            e.printStackTrace()
        } catch (e: NoSuchAlgorithmException) {
            e.printStackTrace()
        } catch (e: KeyManagementException) {
            e.printStackTrace()
        } catch (e: IOException) {
            e.printStackTrace()
        }
    }
}

NetworkModule

@Singleton
@Provides
fun provideOkHttpClient(
    selfSigningHelper: SelfSigningHelper
): OkHttpClient {
    val httpLoggingInterceptor = HttpLoggingInterceptor()
        .setLevel(HttpLoggingInterceptor.Level.BODY)
    return OkHttpClient.Builder()
        .sslSocketFactory(
            selfSigningHelper.sslContext.socketFactory,
            selfSigningHelper.tmf.trustManagers[0] as X509TrustManager
        )
        .addInterceptor(httpLoggingInterceptor)
        .build()
}

...

 

OkHttp는 Retrofit의 Client로 적용되고 있기 때문에 앞서 발생한 오류를 발생시키지 않고 정상적인 응답을 확인할 수 있습니다 :)

728x90
반응형