テスト用に通信鯖をandroid内部に立てたい(SSL対応)の試行錯誤

関連記事のまとめ

今回は

MockWebServerで、httpsのレスポンスをテストしたい為の試行錯誤メモです。

残念ながら解決はできていません。とりあえず現状の状況を書き出してみる感じかと

なぜSSL鯖のテストをしたいのか

にも絡む話なのですが、Appleの方針で、通信に関してSSLを使うことを推奨しています。

ただ各社苦戦している所もやはり多いようで、現在のところリジェクトする対応までは

現時点では保留延長されている状況のようです*1

でどこらへんが問題になるのか?

  • https状況下での
    • ページのリダイレクト
    • JSの実行

等のようですね。でできれば事前にココらへんのテストもしたいわけです

実際の試行状態

実装コード

  • app/debug/java/DebugHogeApplication.java
public class DebugHogeApplication extends HogeApplication {
    private MockWebServer server;

    //〜中略〜
    // 詳細は debugする時に追加している記述の備忘録(2)/(3) 参照


    private boolean useHttps = true;
    private void initServer(){
        server = new MockWebServer();
        
    //SSLの初期化設定
    if(useHttps){  //★ 今回追加
             useHttps = setUseHttps();
        }
        
        Dispatcher dispatcher = new Dispatcher() {

            @Override
            public MockResponse dispatch(final RecordedRequest request) throws InterruptedException {
                if (request == null || request.getPath() == null) {
                    return new MockResponse().setResponseCode(400);
                }
                
                MockResponse res = new MockResponse()
                                        .addHeader("Content-Type", "application/json") 
                                        .addHeader("charset", "utf-8") 
                                        .setResponseCode(200);

                MockResponse resB = new MockResponse()
                                        .addHeader("Content-Type", "image/png") 
                                        .setResponseCode(200);

                String path = request.getPath();

                String BASE_MAIN_URL = RestUtil.createBaseUrl().toString();
                if (path.indexOf(BASE_MAIN_URL + "/hoge") != -1) {
                    return res.setBody(readJson("hoge.json"));
                }
                if (path.indexOf(BASE_MAIN_URL + "/fuga") != -1) {
                    return res.setBody(readJson("fuga.json"));
                }
                if (path.indexOf(BASE_MAIN_URL + "/maiu") != -1) {
                    MockResponse resB = new MockResponse()
                            .addHeader("Content-Type", "image/png")
                            .setResponseCode(200);
                    return resB.setBody(readBinary("sample.png"));
                }

                return new MockResponse().setResponseCode(404);
            }
        };

        server.setDispatcher(dispatcher);

        try{
            server.start();
            
            if(!useHttps){ //★ 今回追加
                RestUtil.BASE_SCHEME = RestUtil.SCHEME_HTTP;            
            }
            RestUtil.BASE_HOST = server.getHostName();
            RestUtil.BASE_PORT = server.getPort();
        }catch(Exception ex){
            shutdown();
            server = null;     
        }
    }


    private boolean setUseHttps(){
        try {
            AssetManager assetManager = getApplicationContext().getResources().getAssets();

            char[] serverKeyStorePassword = "android".toCharArray();

            InputStream stream = assetManager.open("mystore.bks");
            KeyStore serverKeyStore = KeyStore.getInstance("BKS");
            serverKeyStore.load(stream, serverKeyStorePassword);


            String kmfAlgoritm = KeyManagerFactory.getDefaultAlgorithm();
            KeyManagerFactory kmf = KeyManagerFactory.getInstance(kmfAlgoritm);
            kmf.init(serverKeyStore, serverKeyStorePassword);

            TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(kmfAlgoritm);
            trustManagerFactory.init(serverKeyStore);


            SSLContext sslContext = SSLContext.getInstance("TLS");
            sslContext.init(kmf.getKeyManagers(), trustManagerFactory.getTrustManagers(), null);
            SSLSocketFactory sf = sslContext.getSocketFactory();
            server.useHttps(sf, false);
            return true;
        } catch (Exception e) {
            Log.e(TAG,"setUseHttps",e);
        }

        return false;
    }

assetに置く 証明書の作成

crazybob.org: Android: Trusting SSL certificates を読みながら

以下のshで作成

  • mystore.bks
  • app/debug/asset に配置

  • mycert.pem は上記のサイトのものをそのまま流用*2
  • bcprov-jdk15on-1.56.jar の方を利用*3

  • create_bks.sh

export CLASSPATH=`pwd`/bcprov-jdk15on-1.56.jar
CERTSTORE=mystore.bks
if [ -a $CERTSTORE ]; then
    rm $CERTSTORE || exit 1
fi

keytool \
      -import \
      -v \
      -trustcacerts \
      -alias 0 \
      -file <(openssl x509 -in mycert.pem) \
      -keystore $CERTSTORE \
      -storetype BKS \
      -provider org.bouncycastle.jce.provider.BouncyCastleProvider \
      -providerpath `pwd`/bcprov-jdk15on-1.56.jar \
      -storepass android

実行結果

  • MockWebServerのuseHttps 自体は成功
  • Clientサイドから上記のローカル鯖にアクセスしようとすると下記のエラーが発生する
    • OS5/OS6 で確認
javax.net.ssl.SSLProtocolException: SSL handshake terminated: ssl=0x7f7beb3f80: Failure in SSL library, usually a protocol error
error:100c543e:SSL routines:ssl3_read_bytes:TLSV1_ALERT_INAPPROPRIATE_FALLBACK (external/boringssl/src/ssl/s3_pkt.c:972 0x7f66a3ce20:0x00000001)
  • 考えられるのは、
    • 下記のページのクライアント側Okhttp3 Clientを加工していないから?
    • でもできれば実装コードはあまりいじりたくないなorz

比較的な話

のnanohttp もSSL対応できるっぽいんだけど、SSLの立て方とはちがうんだよな・・・

という所で詰まってしまっている感じだったりします

GitHub - NanoHttpd/nanohttpd: Tiny, easily embeddable HTTP server in Java. のreadme.mdに書いてある

keytool -genkey -keyalg RSA -alias selfsigned -keystore keystore.jks -storepass android -validity 360 -keysize 2048 -ext SAN=DNS:localhost,IP:127.0.0.1  -validity 9999

で作るキーだと、JKSの証明書Androidにはないのでエラーになるんですよね・・*4

上記の記載例は、普通にPC on Gradle でnanohttpの鯖を立ち上げる設定なのだろうか・・

2/22追加検証

の 不明の認証局 のあたり

みて再チャレンジしてみたけど、やっぱり駄目だった・・*5

うーん。どうしたらいいんだろう?知見者のアドバイスがほしい。。

    private boolean setUseHttps(){
        try {
            AssetManager assetManager = getApplicationContext().getResources().getAssets();

            char[] serverKeyStorePassword = "android".toCharArray();
/*
            InputStream stream = assetManager.open("mystore.bks");
            KeyStore serverKeyStore = KeyStore.getInstance("BKS");
            serverKeyStore.load(stream, serverKeyStorePassword);
*/
            //変更箇所
            InputStream stream = assetManager.open("server.der.crt");

            CertificateFactory cf = CertificateFactory.getInstance("X.509");
            // From https://www.washington.edu/itconnect/security/ca/load-der.crt
            InputStream caInput = new BufferedInputStream(stream);
            Certificate ca;
            try {
                ca = cf.generateCertificate(caInput);
                System.out.println("ca=" + ((X509Certificate) ca).getSubjectDN());
            } finally {
                caInput.close();
            }

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

            String kmfAlgoritm = KeyManagerFactory.getDefaultAlgorithm();
            KeyManagerFactory kmf = KeyManagerFactory.getInstance(kmfAlgoritm);
            kmf.init(serverKeyStore, serverKeyStorePassword);

            TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(kmfAlgoritm);
            trustManagerFactory.init(serverKeyStore);


            SSLContext sslContext = SSLContext.getInstance("TLS");
            sslContext.init(kmf.getKeyManagers(), trustManagerFactory.getTrustManagers(), null);
            SSLSocketFactory sf = sslContext.getSocketFactory();
            server.useHttps(sf, false);
            return true;
        } catch (Exception e) {
            Log.e(TAG,"setUseHttps",e);
        }

        return false;
    }

*1:少なくとも4月一杯は無いよう

*2:自分で作成できなかったので・・

*3:MockWebServer はこちらの方を利用しているため。Squareさんがpatchを当てている感じのjarなのでこちらを使う

*4:AndroidだとBKSの証明書のみのはず。。

*5:証明書のセットまではできるけど・・・