【Android】HttpsURLConnection實作憑證綁定的方法

HttpsURLConnection with certificate pinning.

Posted by Tabaco on May 19, 2018

APP憑證綁定

什麼是憑證綁定(Certificate Pinning)?簡單的來說,憑證綁定是防止攻擊者使用假憑證進行中間人攻擊的一種安全機制。換言之,若未確實做到憑證綁定,則有心人士便可利用假憑證嗅探傳輸中的加密內容,以MITM(Man-in-the-middle)手法攔截敏感資訊。

參考資料:

網路安全性設定

Android N(API 24)以後可在APP資源內新增安全性設定檔以防止MITM攻擊,但此方法只侷限在API 24以後Android版本,API 24以前仍然需要使用憑證綁定方式防止MITM攻擊。

新增res/xml/network_security_config.xml,這邊我以github作為例子。

<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
    <domain-config>
        <domain includeSubdomains="true">github.com</domain>
        <pin digest="SHA-256">pL1+qb9HTMRZJmuC/bB/ZI9d302BYrrqiVuRyW+DGrU=</pin>
        <!-- backup pin -->
        <pin digest="SHA-256">RRM1dGqnDFsCJXBTHky16vi1obOlCgFFn/yOhI/y+ho=</pin>
    </domain-config>
</network-security-config>

Androidmanifest.xml新增android:networkSecurityConfig="@xml/network_security_config"

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="me.tabacowang.sslpinning">
    <uses-permission android:name="android.permission.INTERNET"/>
    <application

        ...

        android:networkSecurityConfig="@xml/network_security_config">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>
</manifest>

HttpsURLConnection憑證綁定

接著進入業配主題(終於啊~),這邊將展示如何作憑證綁定,詳細的程式碼可以參考SSLPinning

對OkHttp憑證榜定方式有興趣的可以看我另一篇文章:OkHttp實作憑證綁定的方法

github網站上下載憑證,放在assets資料夾內

利用getAssets()取得憑證檔。

private SSLContext getPinnedSSLContext() throws IOException {
    InputStream input = null;
    try {
        input = getActivity().getAssets().open("githubcom.crt");
        return PinnedSSLContextFactory.getSSLContext(input);
    } finally {
        if (null != input) {
            input.close();
        }
    }
}

建立憑證綁定的SSLContext實例。

public class PinnedSSLContextFactory {

    private static final String TAG = "PinnedSSLContextFactory";

    /**
     * Creates a new SSLContext instance, loading the CA from the input stream.
     *
     * @param input InputStream with CA certificate.
     * @return The new SSLContext instance.
     */
    public static SSLContext getSSLContext(InputStream input) {
        try {
            Certificate ca = loadCertificate(input);
            KeyStore keyStore = createKeyStore(ca);
            TrustManager[] trustManagers = createTrustManager(keyStore);
            return createSSLContext(trustManagers);
        } catch (CertificateException e) {
            Log.e(TAG, "Failed to create certificate factory", e);
        } catch (KeyStoreException e) {
            Log.e(TAG, "Failed to get key store instance", e);
        } catch (KeyManagementException e) {
            Log.e(TAG, "Failed to initialize SSL Context", e);
        }
        return null;
    }

    /**
     * Loads CAs from an InputStream. Could be from a resource or ByteArrayInputStream or from
     * https://www.washington.edu/itconnect/security/ca/load-der.crt.
     *
     * @param input InputStream with CA certificate.
     * @return Certificate
     * @throws CertificateException If certificate factory could not be created.
     */
    private static Certificate loadCertificate(InputStream input) throws CertificateException {
        CertificateFactory cf = CertificateFactory.getInstance("X.509");
        return cf.generateCertificate(input);
    }

    /**
     * Creates a key store using the certificate.
     *
     * @param ca Certificate to trust
     * @return KeyStore containing our trusted CAs.
     * @throws KeyStoreException
     */
    private static KeyStore createKeyStore(Certificate ca) throws KeyStoreException {
        try {
            String keyStoreType = KeyStore.getDefaultType();
            KeyStore keyStore = KeyStore.getInstance(keyStoreType);
            keyStore.load(null, null);
            keyStore.setCertificateEntry("ca", ca);
            return keyStore;
        } catch (IOException e) {
            Log.e(TAG, "Could not load key store", e);
        } catch (NoSuchAlgorithmException e) {
            Log.e(TAG, "Could not load key store", e);
        } catch (CertificateException e) {
            Log.e(TAG, "Could not load key store", e);
        }
        return null;
    }

    /**
     * Creates a TrustManager that trusts the CAs in our KeyStore.
     *
     * @param keyStore Key store with certificates to trust.
     * @return TrustManager that trusts the CAs in our key store.
     * @throws KeyStoreException If initialization fails.
     */
    private static TrustManager[] createTrustManager(KeyStore keyStore) throws KeyStoreException {
        try {
            String tmfAlgorithm = TrustManagerFactory.getDefaultAlgorithm();
            TrustManagerFactory tmf = TrustManagerFactory.getInstance(tmfAlgorithm);
            tmf.init(keyStore);
            return tmf.getTrustManagers();
        } catch (NoSuchAlgorithmException e) {
            Log.e(TAG, "Failed to get trust manager factory with default algorithm", e);
        }
        return null;
    }

    /**
     * Creates an SSL Context that uses a specific trust manager.
     *
     * @param trustManagers Trust manager to use.
     * @return SSLContext that uses the trust manager.
     * @throws KeyManagementException
     */
    private static SSLContext createSSLContext(TrustManager[] trustManagers) throws
            KeyManagementException {
        try {
            SSLContext context = SSLContext.getInstance("TLS");
            context.init(null, trustManagers, null);
            return context;
        } catch (NoSuchAlgorithmException e) {
            Log.e(TAG, "Failed to initialize SSL context with TLS algorithm", e);
        }
        return null;
    }
}

最後透過openConnection建立連線。

private String connect(URL url) throws IOException {
    URLConnection connection = url.openConnection();
    if (null != mSSLContext && connection instanceof HttpsURLConnection) {
        ((HttpsURLConnection) connection).setSSLSocketFactory(mSSLContext.getSocketFactory());
    }
    InputStream in = connection.getInputStream();
    return readStream(in);
}