문제상황
안드로이드에서는 버전 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.
원인을 찾아보니 요청 주소에서 요구하는 SSL 인증서를 다운받아 OkHttp 객체에 적용시켜줘야 했습니다.
SSL 인증서를 적용해보자.
적용
저희 팀의 서버 주소는 공개할 수 없지만 API를 요청하는 도메인 주소에 접속해 검색바 좌측 '사이트 정보 보기'에서 인증서를 다운받을 수 있습니다.
다운받은 인증서는 프로젝트의 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로 적용되고 있기 때문에 앞서 발생한 오류를 발생시키지 않고 정상적인 응답을 확인할 수 있습니다 :)