android ssl pinning(HPKP -HTTP Public Key Pinning) 적용 방법

2019. 8. 20. 22:45 개발 이야기/android

SSL pinning 적용방법에 대해서 알아보겠습니다!

 

retrofit 기반의 소스코드라는 점! 참고해주세요

 

먼저! SSL pinning이 뭐냐? 이거 왜할까요 ?


중간자 공격(man in the middle attack, MITM)은 네트워크 통신을 조작하여 통신 내용을 도청하거나 조작하는 공격 기법이다. 중간자 공격은 통신을 연결하는 두 사람 사이에 중간자가 침입하여, 두 사람은 상대방에게 연결했다고 생각하지만 실제로는 두 사람은 중간자에게 연결되어 있으며 중간자가 한쪽에서 전달된 정보를 도청 및 조작한 후 다른 쪽으로 전달한다.

많은 암호 프로토콜은 중간자 공격을 막기 위하여 인증을 사용한다. 예를 들어, TLS/SSL 프로토콜은 공개 키를 기반으로 한 인증을 사용한다.

 


MITM 공격 중 하나인 WEB Proxy 공격을 막는 방법인데요!

 

인증서를 고정시켜서!

즉, 내 서버에 맞는 인증서만 통신이 가능하도록해서 !

proxy 공격에 대응하는 것입니다

 


기존에 SSL pinning 우회 방법을 블로그에 올렸었는데요

필요하신 분들은 참고해주세요!

SSL Pining 우회 iOS


 

적용 방법에 대해서 많이 알려진 내용이 없어서 알아보도록 하겠습니다!

 

자 먼저 가장 필요한 것은 당연히! 인증서입니다.

 

인증서를 

https://www.comodossl.co.kr/

코도모 같은 곳에서 구매하게 되면 

이런 식으로 인증서가 담긴 압축파일을 주는데요!

이 중에 .crt 확장자 파일 중 key가 없는 파일을 열어보면 

 

하기와 같이 된 인증서가 존재합니다.

 

이 파일을 안드로이드 앱 디렉터리에 res 디렉터리 하위에 raw 디렉터리에 넣어줍니다

 

 

그리고 3가지의 함수를 만들어서 쓸건데요

먼저 인증서를 통해서 KeyStore를 리턴하는 함수!

private static KeyStore getKeyStore(Context context){
        try {
            CertificateFactory cf = CertificateFactory.getInstance("X.509");
            InputStream caInput = context.getResources().openRawResource(R.raw.인증서파일이름!);
            Certificate ca = null;
            try {
                ca = cf.generateCertificate(caInput);
                System.out.println("ca=" + ((X509Certificate) ca).getSubjectDN());
            } catch (CertificateException e) {
                e.printStackTrace();
            } finally {
                caInput.close();
            }


            String keyStoreType = KeyStore.getDefaultType();
            KeyStore keyStore = KeyStore.getInstance(keyStoreType);
            keyStore.load(null, null);
            if (ca == null) {
                return null;
            }
            keyStore.setCertificateEntry("ca", ca);
            return  keyStore;

        }catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (KeyStoreException e) {
            e.printStackTrace();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

이렇게 keystore를 리턴하는 함수를 만들어준 후!

 

SSLSocketFactory 함수를 만들어 줍니다

static SSLSocketFactory getPinnedCertSslSocketFactory(Context context) {
        try {

            String tmfAlgorithm = TrustManagerFactory.getDefaultAlgorithm();
            TrustManagerFactory tmf = TrustManagerFactory.getInstance(tmfAlgorithm);
            tmf.init(getKeyStore(context));

            SSLContext sslContext = SSLContext.getInstance("TLS");
            sslContext.init(null, tmf.getTrustManagers(), null);

            return sslContext.getSocketFactory();
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        } catch (KeyStoreException e) {
            e.printStackTrace();
        } catch (KeyManagementException e) {
            e.printStackTrace();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

 

그리고나서 x509TrustManager 클래스를 만들어 줄껀데요

하기와 같이 만들어 줍니다! 

 

package 패키지명

......

public class x509TrustManager implements X509TrustManager {
    private final X509TrustManager originalX509TrustManager;
    private final KeyStore trustStore;

    public x509TrustManager(KeyStore trustStore) throws NoSuchAlgorithmException, KeyStoreException {
        this.trustStore = trustStore;

        TrustManagerFactory originalTrustManagerFactory = TrustManagerFactory.getInstance("X509");
        originalTrustManagerFactory.init((KeyStore) null);

        TrustManager[] originalTrustManagers = originalTrustManagerFactory.getTrustManagers();
        originalX509TrustManager = (X509TrustManager) originalTrustManagers[0];
    }

    @Override
    public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {

    }

    @Override
    public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
        try {
            originalX509TrustManager.checkServerTrusted(chain, authType);
        } catch (CertificateException originalException) {
            try {
                X509Certificate[] reorderedChain = reorderCertificateChain(chain);
                CertPathValidator validator = CertPathValidator.getInstance("PKIX");
                CertificateFactory factory = CertificateFactory.getInstance("X509");
                CertPath certPath = factory.generateCertPath(Arrays.asList(reorderedChain));
                PKIXParameters params = new PKIXParameters(trustStore);
                params.setRevocationEnabled(false);
                validator.validate(certPath, params);
            } catch (Exception ex) {
                throw originalException;
            }
        }
    }

    @Override
    public X509Certificate[] getAcceptedIssuers() {
        return new X509Certificate[0];
    }

    private X509Certificate[] reorderCertificateChain(X509Certificate[] chain) {

        X509Certificate[] reorderedChain = new X509Certificate[chain.length];
        List<X509Certificate> certificates = Arrays.asList(chain);

        int position = chain.length - 1;
        X509Certificate rootCert = findRootCert(certificates);
        reorderedChain[position] = rootCert;

        X509Certificate cert = rootCert;
        while ((cert = findSignedCert(cert, certificates)) != null && position > 0) {
            reorderedChain[--position] = cert;
        }

        return reorderedChain;
    }

    private X509Certificate findRootCert(List<X509Certificate> certificates) {
        X509Certificate rootCert = null;

        for (X509Certificate cert : certificates) {
            X509Certificate signer = findSigner(cert, certificates);
            if (signer == null || signer.equals(cert)) { // no signer present, or self-signed
                rootCert = cert;
                break;
            }
        }

        return rootCert;
    }

    /**
     * A helper method for certificate re-ordering.
     * Finds the first certificate in the list of certificates that is signed by the sigingCert.
     */
    private X509Certificate findSignedCert(X509Certificate signingCert, List<X509Certificate> certificates) {
        X509Certificate signed = null;

        for (X509Certificate cert : certificates) {
            Principal signingCertSubjectDN = signingCert.getSubjectDN();
            Principal certIssuerDN = cert.getIssuerDN();
            if (certIssuerDN.equals(signingCertSubjectDN) && !cert.equals(signingCert)) {
                signed = cert;
                break;
            }
        }

        return signed;
    }

    /**
     * A helper method for certificate re-ordering.
     * Finds the certificate in the list of certificates that signed the signedCert.
     */
    private X509Certificate findSigner(X509Certificate signedCert, List<X509Certificate> certificates) {
        X509Certificate signer = null;

        for (X509Certificate cert : certificates) {
            Principal certSubjectDN = cert.getSubjectDN();
            Principal issuerDN = signedCert.getIssuerDN();
            if (certSubjectDN.equals(issuerDN)) {
                signer = cert;
                break;
            }
        }

        return signer;
    }
}

 

사실은 x509TrustManager 클래스는 없어도 무관한데요! 

x509TrustManager 만들어주는 이유는 구글 정책에 맞게 개발하기 위해서 입니다

아래서 사용될 sslSocketFactory에 파라미터로 

SSLSocketFactory와 X509TrustManager가 들어가게 만들어주는 것을 권고하고 있더라구요

@deprecated 다 아시졍 ?

 

그러고 나서 OkHttpClient를 생성해주는

getSafeOkHttpClient 함수를 만들어줍니다

x509TrustManager 를 생성하기 위해서는 keystore가 필요한데요 

아래와 같이 생성해주고

sslSocketFactory 파라미터로 getPinnedCertSslSocketFactory와 함께 넣어줍니다.

 

패킷 로깅용 인터셉터는 제외시켜주셔도 되요!

static OkHttpClient getSafeOkHttpClient(Context context) {
        OkHttpClient.Builder builder = new OkHttpClient.Builder();

        x509TrustManager x509TrustManager = null;
        try {
            x509TrustManager = new x509TrustManager(getKeyStore(context));
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        } catch (KeyStoreException e) {
            e.printStackTrace();
        }

        HttpLoggingInterceptor interceptor = new HttpLoggingInterceptor(); // 패킷 로깅
        interceptor.setLevel(HttpLoggingInterceptor.Level.BODY); //패킷 로깅

        return builder
                .sslSocketFactory(Objects.requireNonNull(getPinnedCertSslSocketFactory(context)), Objects.requireNonNull(x509TrustManager))
                .addNetworkInterceptor(new AddCookiesInterceptor())
                .addInterceptor(new ReceivedCookiesInterceptor())
                .addInterceptor(interceptor) //패킷 로깅
                .build();
    }

 

이렇게 만들어 준 후 하기와 같이 사용해주시면 됩니다!

 

public class APIClient {
    private static Retrofit retrofit = null;

    public static Retrofit getClient(Context context) {
        OkHttpClient client = SSLUtil.getSafeOkHttpClient(context);
        retrofit = new Retrofit.Builder()
                .baseUrl(Config.URL)
                .addConverterFactory(GsonConverterFactory.create())
                .client(client)
                .build();
        return retrofit;
    }
}

 

이렇게 적용된 SSL 통신을 했을때

 

웹 프록시 도구인 Burp suite를 이용해 Burp 인증서를 설치한 후!

프록시를 시도하게되면 어떻게 될까요!? 

 

이렇게 SSL 핸드쉐이크 에러가 발생하게 됩니다!

 

그럼! 이상으로 retrofit을 이용한 ssl pinning 적용방법이었습니다.