Android v4.1~v4.4のTLS v1.2対応

さくらインターネットが5月でTLS1.0/1.1が使えなくなるため、cocos2d-xで作ったゲームがAndroid v4.1からv4.4(API Level 16~19)において通信ができなくなる問題が起きました。
SSLSocket参考

基本的には以下のところで紹介された方法でやりましたが、cocos2d-x周りに少し変更が必要でした。
Android 4.1+ enable TLS 1.1 and TLS 1.2
ソースコードはこちらです。

今回対応が必要としたcocos2d-xのバージョンは3.8.1です。

  1. まずは以下の内容でTLSSocketFactory.javaファイルを作ります。
    package org.cocos2dx.lib;
    
    import java.io.IOException;
    import java.net.InetAddress;
    import java.net.Socket;
    import java.net.UnknownHostException;
    import java.security.KeyManagementException;
    import java.security.NoSuchAlgorithmException;
    
    import javax.net.ssl.SSLContext;
    import javax.net.ssl.SSLSocket;
    import javax.net.ssl.SSLSocketFactory;
    
    /**
     * @author fkrauthan
     */
    public class TLSSocketFactory extends SSLSocketFactory {
    
        private SSLSocketFactory internalSSLSocketFactory;
    
        public TLSSocketFactory() throws KeyManagementException, NoSuchAlgorithmException {
            SSLContext context = SSLContext.getInstance("TLS");
            context.init(null, null, null);
            internalSSLSocketFactory = context.getSocketFactory();
        }
    
        public TLSSocketFactory(SSLSocketFactory sslSocketFactory) throws KeyManagementException, NoSuchAlgorithmException {
            internalSSLSocketFactory = sslSocketFactory;
        }
    
        @Override
        public String[] getDefaultCipherSuites() {
            return internalSSLSocketFactory.getDefaultCipherSuites();
        }
    
        @Override
        public String[] getSupportedCipherSuites() {
            return internalSSLSocketFactory.getSupportedCipherSuites();
        }
        
        @Override
        public Socket createSocket() throws IOException {
            return enableTLSOnSocket(internalSSLSocketFactory.createSocket());
        }
    
        @Override
        public Socket createSocket(Socket s, String host, int port, boolean autoClose) throws IOException {
            return enableTLSOnSocket(internalSSLSocketFactory.createSocket(s, host, port, autoClose));
        }
    
        @Override
        public Socket createSocket(String host, int port) throws IOException, UnknownHostException {
            return enableTLSOnSocket(internalSSLSocketFactory.createSocket(host, port));
        }
    
        @Override
        public Socket createSocket(String host, int port, InetAddress localHost, int localPort) throws IOException, UnknownHostException {
            return enableTLSOnSocket(internalSSLSocketFactory.createSocket(host, port, localHost, localPort));
        }
    
        @Override
        public Socket createSocket(InetAddress host, int port) throws IOException {
            return enableTLSOnSocket(internalSSLSocketFactory.createSocket(host, port));
        }
    
        @Override
        public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort) throws IOException {
            return enableTLSOnSocket(internalSSLSocketFactory.createSocket(address, port, localAddress, localPort));
        }
    
        private Socket enableTLSOnSocket(Socket socket) {
            if(socket != null && (socket instanceof SSLSocket)) {
                ((SSLSocket)socket).setEnabledProtocols(new String[] {"TLSv1.2"});
            }
            return socket;
        }
    }
    

    ここ変えたのは最初のパッケージ名と最後のTLSv1.1を消すぐらいです。さくらインターネットはTLSv1.1も無効にするので不要かと。

  2. 上記TLSSocketFactory.javaをcocos2dxプロジェクトルート/cocos2d/cocos/platform/android/java/src/org/cocos2dxに保存します。
  3. /cocos2d/cocos/platform/android/java/src/org/cocos2dx/lib/Cocos2dxHttpURLConnection.javaを変更します。
    1. 54行あたりに以下を追加
      import org.cocos2dx.lib.TLSSocketFactory;
      import javax.net.ssl.SSLSocketFactory;
      
    2. 131行あたりに以下のをコメントアウト
            SSLContext context = SSLContext.getInstance("TLS");
            context.init(null, tmf.getTrustManagers(), null);
      
            httpsURLConnection.setSSLSocketFactory(context.getSocketFactory());
      
    3. 上記コメントアウトの箇所すぐ下に以下のを追加
                  SSLSocketFactory noSSLv3Factory = new TLSSocketFactory();
                  httpsURLConnection.setSSLSocketFactory(noSSLv3Factory);
      
  4. 本来ここまでやれば良いはずですが、cocosはssl認証のような面倒なことはやってくれませんでした。つまり、通常HTTPS通信行う時、上記変えた箇所のsetVerifySSL(HttpURLConnection urlConnection, String sslFilename)メソッドには通っていないんです。
    そのため、こちらのSSL証明書をチェックさせる方法で無理に通らせることにしました。
    HttpRequestを作る前に以下を追加しました。

        auto path = FileUtils::getInstance()->fullPathForFilename("cacert.pem");
        HttpClient::getInstance()->setSSLVerification(path);
    

    ちなみに、このcacert.pemはどこから来たかというと、自分サイトから以下のコマンド実行すると得られます。

    openssl s_client -showcerts -connect ホスト名:443 </dev/null 2>/dev/null|openssl x509 -outform PEM >cacert.pem
    

    そしてこのpemファイルをresディレクトリなどの場所に置き、Xcodeのプロジェクトにも追加すれば良いでしょう。

    How to save a remote server SSL certificate locally as a fileのelec3647の回答
    余談になりますが、実際全く違うサイトの証明書を落としてきて試したところ、ここのca証明書なんでもいいようです。
    /cocos2d/cocos/network/HttpClient-android.cppの541行あたりのsetVerifySSL()を見ると分かりますが、まずはssl caファイルをチェックし、それがなければ狙いのsetVerifySSLに通してくれないので、あまり意味がなくても設定します。
    ※あっ!もしかして、そのif(_client->getSSLVerification().empty()) returnをコメントアウトする手もあったでは?!

  5. 最後に/cocos2d/cocos/network/HttpAsynConnection-apple.mの193行あたりに立て続け[(id)certArrayRef release];が2つありますので、そのうちの一つをコメントアウト(削除)します。
    ここでもcocosのssl証明書に関するやる気のなさが窺えます。

これでAndroid v4.1~v4.4の端末でも問題なくHttpRequestで通信できるはずです。

お気づきの方もいらっしゃると思いますが、Cocos2dxHttpURLConnection.javaのsetVerifySSL(HttpURLConnection urlConnection, String sslFilename)メソッド中では、SSLContext作る前に証明書やキーなどいろいろと準備してましたが、今回の変更で全く使われなくなります。

こちらのgotevが紹介した方法でやりたくなります(実際やりたくなりました^^;)が、下記のエラーが出てしまいます。

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

ここは深入りしてませんが、つまりtmf.getTrustManagers()を渡してcontext.init(null, tmf.getTrustManagers(), null);するとパスが見つからないというエラーが出ますが、context.init(null, null, null);にするといいです。
この問題はstevieが言ってることがを参考にするといいかもしれません。

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください