LoginSignup
8
11

More than 5 years have passed since last update.

改造する人のためのJSSE

Last updated at Posted at 2016-08-29

IoTとやらが大ブームですが、その一方で頭を抱えたくなるような脆弱性満載のTLSスタックを搭載した製品が時折みられます。みなさんTLSスタックのテストちゃんとやってますか。

TLSスタックのテストをやるとなると、そこで使うスタブとかモックとかダブルとかドライバとか呼ばれるモロモロのあれやそれやを作っていくわけですが、そのためにTLSスタックを一から書くのはちょっと非現実的なので、何らかのライブラリを入手してそこから手を加えていくことになると思います。しかるにOpenSSLは、高機能ですけど改造母体とするには敷居が高いですよね。しかしOpenJDKのJSSEなら、機能性はともかく、改造母体としての扱いやすさはそれなりに悪くないという印象です。

というわけで、くれぐれも悪用厳禁でレッツゴー!

実行環境と方針

OracleJDK/OpenJDKの標準ライブラリをextendして機能追加できればいいんですけどそれは限界があるので、OpenJDK8uのJSSEソースを手に入れて、それを標準ライブラリと重複しないようにパッケージ名を書き換えて使います。今回使ったバージョンは以下のとおり。

Oracle JDK上で、OpenJDKのJSSE部分だけを切り出してきたものを動かします。それ本当に大丈夫なのかという話ですが、意外と大体まあまあおおむねそこそこそれなりに動くと言っていいんじゃないでしょうかね、といったところです(汗)。

ダウンロード

wget -q -O- http://hg.openjdk.java.net/jdk8u/jdk8u/jdk/archive/5beaee665e14.tar.bz2/src/share/classes/sun/security/ssl/ | tar xjvf -

これで展開されます。
OpenJDKのバージョン管理システムはMercurialなので、hgコマンドを駆使してダウンロードしてもいいのですが、今回使うのはsrc/share/classes/sun/security/ssl/の下のファイルだけですから、これで十分です。

パッケージ名の書き換え

オリジナルはsun.security.sslですが、このままではOracle JDKのオリジナル版と重複してしまうので、書き換えを行います。今回はcom.example.ssltesterという名前にしてみました。このあたりはお好みで適宜変更してください。

mkdir -p src/com/example/ssltester/krb5
for i in `find jdk-5beaee665e14/ -type f -print`
do
j=`echo $i | sed s,jdk-5beaee665e14/src/share/classes/sun/security/ssl/,src/com/example/ssltester/,`
sed -e 's/sun\.security\.ssl/com.example.ssltester/' < $i > $j
done

エラーの修正

一箇所、エラーが出てしまいますが、ここは安直にコメントアウトで対応します。

diff --git a/src/com/example/ssltester/SunJSSE.java b/src/com/example/ssltester/SunJSSE.java
index f7883a4..59d8a3d 100644
--- a/src/com/example/ssltester/SunJSSE.java
+++ b/src/com/example/ssltester/SunJSSE.java
@@ -231,9 +231,9 @@ public abstract class SunJSSE extends java.security.Provider {
     }

     private void subclassCheck() {
-        if (getClass() != com.sun.net.ssl.internal.ssl.Provider.class) {
-            throw new AssertionError("Illegal subclass: " + getClass());
-        }
+//        if (getClass() != com.sun.net.ssl.internal.ssl.Provider.class) {
+//            throw new AssertionError("Illegal subclass: " + getClass());
+//        }
     }

     @Override

これ以外のエラーが出た場合は、OracleJDKとOpenJDKのバージョンを見直してみて下さい。JSSEの内部仕様は案外頻繁に変更されますので、バージョンが違うと動かなくなる場合があるようです。

サンプルプログラム

ソースの展開が完了したので、早速動かしてみます。サンプルは2種類、クライアントとしてサーバに接続しに行くものと、サーバとしてlistenするものです。

クライアントのサンプル

https://localhost/につなぐサンプルです。ポートは標準の443です。

import com.example.ssltester.*;
import java.io.*;
import java.net.*;
import java.security.*;
import java.security.cert.*;
import javax.net.ssl.*;

public class TestClient {

    public static void main(String[] args) throws KeyManagementException, UnknownHostException, IOException, NoSuchAlgorithmException {
        System.setProperty("javax.net.debug", "ssl");

        SSLContext ctx = SSLContext.getInstance("TLSv1.2", new SunJSSE() {});
        ctx.init(null, tm, null);

        URL url = new URL("https://localhost/");
        HttpsURLConnection conn = (HttpsURLConnection)url.openConnection();
        conn.setSSLSocketFactory(ctx.getSocketFactory());

        BufferedReader in = new BufferedReader(new InputStreamReader(conn.getInputStream()));

        while (true) {
            String l = in.readLine();
            if (l == null) {
                break;
            }
            System.out.println(l);
        }

        in.close();
    }

    private static  final TrustManager[] tm = {
            new X509ExtendedTrustManager() {
                @Override
                public void checkClientTrusted(X509Certificate[] arg0, String arg1)
                        throws CertificateException {
                }

                @Override
                public void checkServerTrusted(X509Certificate[] arg0, String arg1)
                        throws CertificateException {
                }

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

                @Override
                public void checkClientTrusted(X509Certificate[] arg0, String arg1, Socket arg2)
                        throws CertificateException {
                }

                @Override
                public void checkClientTrusted(X509Certificate[] arg0, String arg1, SSLEngine arg2)
                        throws CertificateException {
                }

                @Override
                public void checkServerTrusted(X509Certificate[] arg0, String arg1, Socket arg2)
                        throws CertificateException {
                }

                @Override
                public void checkServerTrusted(X509Certificate[] arg0, String arg1, SSLEngine arg2)
                        throws CertificateException {
                }
            }
    };

}

プログラム中でSystem.setProperty("javax.net.debug", "ssl");でデバッグログを出力するように指定していますが、大量に出てきますので、鬱陶しければ外してください。

TrustManager[]を変数tmとして定義していますが、これは動作テストに用意したサーバが自己署名証明書を使っている関係で、デフォルトのTrustManagerをそのまま使うと証明書検証に失敗してしまうため、証明書検証を止める処理をここに入れたものです。それでは困る!という場合は、ctx.initのところをctx.init(null, null, null);に変更すれば、デフォルトのTrustManagerが選択されて証明書検証が正しく行われます。

サーバのサンプル

ポート8443で待機するサーバのサンプルです。https://localhost:8443/にアクセスすると、Hi, there!という素っ気ない応答を返します。

import com.example.ssltester.*;
import com.sun.net.httpserver.*;
import java.io.*;
import java.net.*;
import java.security.*;
import java.security.cert.*;
import javax.net.ssl.*;

public class TestServer {

    private final static String KEYFILE = "cert.p12";
    private final static char[] KEYPASSWORD = "testtest".toCharArray();

    public static void main(String[] args)
            throws KeyStoreException, NoSuchAlgorithmException, KeyManagementException,
            IOException, CertificateException, UnrecoverableKeyException {

        System.setProperty("javax.net.debug", "ssl");

        KeyStore ks = KeyStore.getInstance("pkcs12");
        ks.load(new FileInputStream(KEYFILE), KEYPASSWORD);

        KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
        kmf.init(ks, KEYPASSWORD);

        SSLContext ctx = SSLContext.getInstance("TLS", new SunJSSE() {});
        ctx.init(kmf.getKeyManagers(), null, null);

        HttpsServer svr = HttpsServer.create(new InetSocketAddress(8443), 0);

        svr.setHttpsConfigurator(new HttpsConfigurator(ctx));

        svr.createContext("/", new HttpHandler() {
            @Override
            public void handle(HttpExchange arg0) throws IOException {
                String str = "Hi there!";
                arg0.sendResponseHeaders(200, str.length());
                OutputStream os = arg0.getResponseBody();
                os.write(str.getBytes());
                os.flush();
                os.close();
            }

        });

        svr.start();
    }

}

サーバ秘密鍵と証明書は、PKCS#12形式のcert.p12というファイルに収録してあります。このあたりは利用者各位にて適宜修正して下さい。

Q&A

で、ここから何をすればいいのか

JSSEを頑張って解読して理解して下さい。詳細はここでは述べませんが、それなりに根性が必要です。しかし根性をもってすれば、普通の手段ではできない(不正な)TLSメッセージ送信をいろいろテストできるようになります。
手始めに、java.securityファイルのjdk.tls.disabledAlgorithmsjdk.certpath.disabledAlgorithmsの指定を全部無視する改造にチャレンジしてみてはいかがでしょうか。これをやると、その後の作業が楽になります。もちろん、本番環境に投入するコードでそんなことやっちゃダメですよ!あくまでテストの中での話です。

OpenJDK8u JSSEの機能は十分なのか

困ったことにALPN拡張がないのです。OpenJDK9にはあるので、必要なら手を出してみるのも一興かもしれません。拡張の種類が少ないのは不満ですが、致し方ないところです。

以上!くれぐれも悪用厳禁ということで、幸運を祈る。

リファレンス

「Java Secure Socket Extension (JSSE)リファレンス・ガイド」
https://docs.oracle.com/javase/jp/8/docs/technotes/guides/security/jsse/JSSERefGuide.html

「上級JSSE開発者のためのカスタムSSL」
https://www.ibm.com/developerworks/jp/java/library/j-customssl/

不肖私 (2016)
「SSL/TLS(SSL3.0~TLS1.2)のハンドシェイクを復習する」
http://qiita.com/n-i-e/items/41673fd16d7bd1189a29

8
11
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
11