IoTとやらが大ブームですが、その一方で頭を抱えたくなるような脆弱性満載のTLSスタックを搭載した製品が時折みられます。みなさんTLSスタックのテストちゃんとやってますか。
TLSスタックのテストをやるとなると、そこで使うスタブとかモックとかダブルとかドライバとか呼ばれるモロモロのあれやそれやを作っていくわけですが、そのためにTLSスタックを一から書くのはちょっと非現実的なので、何らかのライブラリを入手してそこから手を加えていくことになると思います。しかるにOpenSSLは、高機能ですけど改造母体とするには敷居が高いですよね。しかしOpenJDKのJSSEなら、機能性はともかく、改造母体としての扱いやすさはそれなりに悪くないという印象です。
というわけで、くれぐれも__悪用厳禁__でレッツゴー!
#実行環境と方針
OracleJDK/OpenJDKの標準ライブラリをextendして機能追加できればいいんですけどそれは限界があるので、OpenJDK8uのJSSEソースを手に入れて、それを標準ライブラリと重複しないようにパッケージ名を書き換えて使います。今回使ったバージョンは以下のとおり。
- Windows x64版Java SE Development Kit 8u102
- OpenJDK8u http://hg.openjdk.java.net/jdk8/jdk8 を見て適当なリビジョンを使います。今回は本稿執筆時の最新版 5beaee665e14 です。
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.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