改造する人のためのJSSE

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

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

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

#実行環境と方針

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

- Windows x64版[Java SE Development Kit 8u102](http://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html)
- OpenJDK8u http://hg.openjdk.java.net/jdk8/jdk8 を見て適当なリビジョンを使います。今回は本稿執筆時の最新版 [5beaee665e14](http://hg.openjdk.java.net/jdk8u/jdk8u/jdk/rev/5beaee665e14) です。

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

#ダウンロード

```bash
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``という名前にしてみました。このあたりはお好みで適宜変更してください。

```bash
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
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の内部仕様は案外頻繁に変更されますので、バージョンが違うと動かなくなる場合があるようです。

#サンプルプログラム

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

##クライアントのサンプル

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

```java
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!``という素っ気ない応答を返します。

```java
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.disabledAlgorithms``や``jdk.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