많은 시간을 들인 게시글입니다. 

무단으로 복사해서 게시하지 말아 주세요.


 

 

앱과 서버를 만들어서 HTTP로 통신하게 만들었다.

만들고 나니 HTTPS로 통신하게 만들고 싶어 졌다.

그리고 문제가 생겼다.

 

1. 에러 발생

 

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

 

직접 제작한 인증서를 서버에 적용하고

안드로이드에서 통신하려 하는데 에러가 발생했다.

 

 

 

 

 

답답한 마음에 파파고한테 물어봤다...

파파고가 알려주기를

 

인증 경로에 대한 신뢰 앵커를 찾을 수없다는데......

무슨 말인지 모르겠닼ㅋㅋㅋㅋ

 

 

 

 

2. 원인 추정

 

 

느낌상 원인은 OpenSSL로 직접 만든 인증서를 

서버에 적용했기 때문에 

프로토콜은 HTTPS이지만 신뢰할 수 없다는 거 아닐까 싶다.

 

추가적으로 찾아보니 공식문서에

"시스템에서 신뢰할 수 없는 CA를 보유했기 때문에 SSLHandshakeException이 발생합니다"

"공개 CA가 아니라 자체 사용을 위해 정부, 회사 또는 교육 기관 같은 조직에서 발급하는 비공개 CA이기 때문"

라는 문구를 찾아볼 수 있었다.

 

3. 해결 방안

이런 문제를 해결을 위해 참고할만한 공식문서가 있다.

 

https://developer.android.com/training/articles/security-ssl?hl=ko#UnknownCa

 

HTTPS 및 SSL을 사용한 보안  |  Android 개발자  |  Android Developers

현재 기술적으로 전송 계층 보안(TLS)이라고 알려진 보안 소켓 레이어(SSL)는 클라이언트와 서버 간의 암호화된 통신을 위한 공통 기본 토대입니다. 애플리케이션이 SSL을 잘못 사용하면 악성 개체

developer.android.com

공식문서의 내용은

특정 CA 집합을 신뢰하도록

HttpsURLConnection에 알리는 방식이다.

 

간략하게 이해하기로 SSL 인증서 중 .crt 형식의 인증서 파일이 있는데

이 파일로 SSL 소켓 팩토리를 만들고 urlConnection에 붙이는 방식이다.

 

이러한 내용을 조금 변경해서

urlConnection 대신 OkHttpClient를 세팅하였고

이렇게 만든 OkHttpClient를 Retrofit에 빌더에 추가해주면

해당 에러는 해결할 수 있었다.

 

 

4. 상세 방법 및 소스

1) raw 디렉터리에 crt 인증서 넣기

 

 - 우선 안드로이드 raw 밑에 raw디렉터리를 만든다.

 - SSL소켓 팩토리를 만들기 위해 crt 인증서를 raw밑에 넣는다.

 

1-1 새 디렉토리

 

1-2 raw 디렉토리 선택

 

1-3 crt 인증서 붙여넣기

 

 

 

 

 

 

 

 

2) 자체 인증 빌더 클래스 생성

 

Retrofit으로 통신을 할 것이기 때문에 OkHttp를 사용해야 한다.

OkHttp.Builder를 받아 SSL소켓 팩토리를 붙인 Builder를

반환하는 메서드를 만들었다.

 

public class SelfSigningHelper {
    private SSLContext sslContext;
    private TrustManagerFactory tmf;

    private SelfSigningHelper() {
        setUp();
    }
    
	// 싱글턴으로 생성
    private static class SelfSigningClientBuilderHolder{

        public static final SelfSigningHelper INSTANCE = new SelfSigningHelper();
    }
	
    public static SelfSigningHelper getInstance() {
        return SelfSigningClientBuilderHolder.INSTANCE;
    }

    public void setUp() {

        CertificateFactory cf;
        Certificate ca;

        InputStream caInput;
        try {
            cf = CertificateFactory.getInstance("X.509");
            // Application을 상속받는 클래스에 
            // Context 호출하는 메서드 ( getAppContext() )를 
            // 생성해 놓았음
            caInput = AppApplication.getAppContext().getResources()
            		.openRawResource(R.raw.my_cert);

            ca = cf.generateCertificate(caInput);
            System.out.println("ca=" + ((X509Certificate) ca).getSubjectDN());

            // Create a KeyStore containing our trusted CAs
            String keyStoreType = KeyStore.getDefaultType();
            KeyStore keyStore = KeyStore.getInstance(keyStoreType);
            keyStore.load(null,null);
            keyStore.setCertificateEntry("ca", ca);

            // Create a TrustManager that trusts the CAs in our KeyStore
            String 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.getTrustManagers(), new java.security.SecureRandom());

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

    public OkHttpClient.Builder setSSLOkHttp(OkHttpClient.Builder builder){

        builder.sslSocketFactory(getInstance().sslContext.getSocketFactory(), 
        	(X509TrustManager)getInstance().tmf.getTrustManagers()[0]);
            
        return builder;
    }
}

 

 - 싱글턴으로 생성, 생성자에서 setUp() 호출

 - setUp() 메서드를 사용해서 SSLContext와 TrustManagerFactory를 생성, 할당

 - setSSLOkHttp(OkHttpClient.Builder builder() 에서 SSL 소켓 팩토리 추가

 

 

 

 

 

 

 

 

 

 

3) Retrofit.Builder에 Client변경

 

Retrofit에서 build 설정으로

사설 SSL이 통신 가능한 client를 지정한다.

 

public class RetrofitFactory {

    public static Retrofit createRetrofit(Context context,String baseUrl) {
        Retrofit retrofit = new Retrofit.Builder()
                .baseUrl(baseUrl)
                .client(createOkHttpClient())
                .build();
        return retrofit;
    }

    private static OkHttpClient createOkHttpClient() {
        SelfSigningHelper helper = SelfSigningHelper.getInstance();
        OkHttpClient.Builder builder = new OkHttpClient.Builder();
        helper.setSSLOkHttp( builder);

        return builder.build();
    }
}

- Retrofit.Builder에 client로 OkHttpClient를 지정

- 새로 만든 OkHttpClient에 위에서 만든 메서드를 활용해서 SSL 통신이 되도록 변경

 

 

** 참고 레트로핏 세팅방법 

 

 

 

 

4) Hostname (ip) not verified 에러

 

 

"javax.net.ssl.SSLPeerUnverifiedException:Hostname 10.0.2.2 not verified"

 

 

위의 내용을 다 처리하면

이제 새로운 에러가 나온다 ㅎㅎㅎㅎ

 

다행히 다음의 내용을 참고할 수 있었다.

 

HTTPS 및 SSL을 사용한 보안  |  Android 개발자  |  Android Developers

현재 기술적으로 전송 계층 보안(TLS)이라고 알려진 보안 소켓 레이어(SSL)는 클라이언트와 서버 간의 암호화된 통신을 위한 공통 기본 토대입니다. 애플리케이션이 SSL을 잘못 사용하면 악성 개체

developer.android.com

해당 사이트에서 원인을 친절하게 알려줬다.

 

현재 통신하고 있는 서버가 올바른 인증서를 제시하는지 확인하는 절차에서 에러가 난 것이다.

 

지금까지 조치한 내용은 인증서 자체가

신뢰할 수 없는 소스라고 해서 에러가 난 것이고

이번 에러는 서버가 올바른 인증서를

주는지에 대해서 검증하는 과정에 난 것이다.

 

결국 자체 SSL이기 때문에 난 것 같은데 

찾아보니 검증 도구를 변경하여 강제로

인증하는 방법이 있다.

 

공식문서에서 

"최소한 앱에서 예상한 위치에 있음을

확인하는 인증 도구로 교체" 

하는 방법이라고 소개하는데 

주의해서 사용하라고 하고 있으니 

주의하면 될 것 같다.

 

// 4. 2) 의 소스를 다음과 같이 변경

	public OkHttpClient.Builder setSSLOkHttp(OkHttpClient.Builder builder,String target){

        builder.sslSocketFactory(getInstance().sslContext.getSocketFactory()
        		, (X509TrustManager)getInstance().tmf.getTrustManagers()[0]);
        
        // 여기서부터 추가
        builder.hostnameVerifier((hostname, session) -> {
            if (hostname.contentEquals(target)) {
                Log.d("test", "Approving certificate for host " + hostname);
                return true;
            }else {
                Log.d("test", "fail " + hostname);
                return false;
            }
        });
        // 여기까지 추가
        return builder;
    }

 - '4. 2) 자체 인증 빌더 클래스 생성'에서 setSSLOkHttp의 메서드를 수정한다.

 - 파라미터로 String target를 받아 hostname.contentEquals(target)에서 검증한다.

 

 

public class RetrofitFactory {

    public static Retrofit createRetrofit(Context context,String baseUrl) {
        Retrofit retrofit = new Retrofit.Builder()
                .baseUrl(baseUrl)
                .client(createOkHttpClient())
                .build();
        return retrofit;
    }

    private static OkHttpClient createOkHttpClient() {
        SelfSigningHelper helper = SelfSigningHelper.getInstance();
        OkHttpClient.Builder builder = new OkHttpClient.Builder();
        
        // 여기 변경
        helper.setSSLOkHttp( builder,"10.0.2.2");

        return builder.build();
    }
}

 - 사용 시 Host의 IP를 입력

 - 인증서가 있을 hostname과 HTTPS를 통해

요청하는 목적지가 동일한지 확인하여 검증

 

 

 

 

 

Retrofit 이제 정상적으로 요청이 간다!!

 

 


참조 사이트 :

gist.github.com/nowke/75037c42171d9ea5ce87a49a982c4c39 

 

 

 

 

 

도움이 되는 글이었다면

로그인이 필요 없는 공감 버튼 꾹 눌러주세요! 

 

 

 

 

 

 

 

+ Recent posts

"여기"를 클릭하면 광고 제거.