はじめに
JavaのSSLSocketを使って暗号化通信してみる。ついでにクライアント証明書も。
まずはサーバ側。
証明書の準備
サーバ側はWebサーバで使う証明書と同じく、サーバ証明書・秘密鍵・CA局証明書がまずは必要。
これをまとめてpkcs12形式にしておく。
クライアント証明書を使う場合はクライアント証明書のCA局証明書も使う。こちらは秘密鍵なしで使えるJKS形式で。
pkcs12に証明書だけ入れてもできそうなのだが、なぜか認識されなかった。
サーバ側証明書の読み込み
サーバ証明書は秘密鍵・CA局証明書とセットにしたpkcs12ファイルを読み込む。
FileInputStreamで開いて、java.security.KeyStore の load で読み込む。
読み込んだら javax.net.ssl.KeyManagerFactory の init に指定する。
クライアント証明書用CA局証明書も KeyStore の load で読み込むが、KeyManagerFactory ではなく、javax.net.ssl.TrustManagerFactory の init に指定する。
クライアント証明書を使わない場合はTrustManagerは不要。
コードとしては以下のようになる。
import java.net.Socket;
import java.security.KeyStore;
import javax.net.ssl.SSLContext;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.SSLServerSocket;
import javax.net.ssl.SSLServerSocketFactory;
import java.io.FileInputStream;
public class App
{
private static void initSSL()
{
try (
FileInputStream p12_file = new FileInputStream("servercert.p12");
FileInputStream jks_file = new FileInputStream("clienttrust.jks");
) {
KeyManagerFactory kmf;
KeyStore ks;
ks = KeyStore.getInstance("pkcs12");
ks.load(p12_file, "passphrase".toCharArray());
kmf = KeyManagerFactory.getInstance("SunX509");
kmf.init(ks, "passphrase".toCharArray());
// ここからはクライアント証明書のためのもの
TrustManagerFactory tmf;
KeyStore ts;
ts = KeyStore.getInstance("JKS");
ts.load(jks_file, "jkspass".toCharArray());
tmf = TrustManagerFactory.getInstance("SunX509");
tmf.init(ts);
}
catch (Exception e) {
System.out.println("initSSL exception: " + e.toString());
}
}
servercert.p12 はサーバ証明書・秘密鍵の pkcs12 ファイル、パスフレーズは passphrase なので、ここは環境に合わせて修正が必要。
同じく clienttrust.jks と jkspass も要修正。
ServerSocketの生成
上で生成した kmf と tmf を使う。(クライアント証明書を使わない場合はkmfのみ)
手順としては、java.net.ssl.SSLContext を生成し、初期化時に kmf, tmf (から取得した鍵・証明書情報) を指定する。
続いて ctx.getServerSocketFactory() で ServerSocketFactory を取得、そこから ServerSocket を作る。
コードとしては以下の通り。
SSLContext ctx = SSLContext.getInstance("TLS");
// クライアント証明書を使用しない場合は tmf.getTrustManagers() のところをnullにする
ctx.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null);
SSLServerSocketFactory ssf = ctx.getServerSocketFactory();
SSLServerSocket sslsvr = (SSLServerSocket)ssf.createServerSocket(443);
// クライアント証明書を使用しない場合は、以下不要
sslsvr.setNeedClientAuth(true);
あとは通常の ServerSocket と同様にクライアントからの connect を accept して Socket を得る。(SSLSocketにキャストできる)
443のところがポート番号なので、それ以外のポートを使う場合は変更する。
pkcs12 の作り方
サーバ証明書・クライアント証明書の pkcs12 は openssl で作る。(多分 keytool でも作れるが)
openssl pkcs12 -export -in <証明書ファイル名> -inkey <秘密鍵ファイル名> -certfile <CA局証明書ファイル名> -out <出力ファイル名>
これを実行するとパスフレーズを設定するため、2回入力する必要がある。
JKSの作り方
Java付属の keytool で作る。
keytool import -file <クライアント証明書用CA局ファイル名> -alias clientca -keystore <出力ファイル名>
alias のところは任意で大丈夫そうだが、keytool を使うときに必要になる場合あり。
サーバプログラムの動作確認
クライアントを作る前に openssl を使って動作確認すると良い。
以下のように実行する。
openssl s_client -connect <サーバホスト名>:<ポート> -cert <クライアント証明書ファイル名(PEM)> -key <秘密鍵ファイル名(PEM)>
また、うまく動かない場合は、実行時の java オプションに -Djavax.net.debug=all とすると色々出力されるのでヒントになるかも。allのところにはsslと指定することもでき、sslにするとオプション指定ができる。
指定できるオプションはここ参照。
TLSのバージョンについて
OpenJDK 17 では、デフォルトで TLS 1.2, 1.3 のみ有効になっている。
SSLServerSocket.setEnabledProtocolsで指定可能。例えば、以下のようにすると TLS 1.3 のみしか受け付けなくなる。
SSLServerSocket sslsvr = (SSLServerSocket)ssf.createServerSocket(443);
sslsvr.setEnabledProtocols(new String[] { "TLSv1.3" });
使えるプロトコルの確認は getEnabledProtocols で可能。
同じように Cipher Suites の変更は setEnabledCipherSuites で可能、確認は getEnabledCipherSuites なので、getEnabledCipherSuites してから、使いたくないものを削って setEnabledCipherSuites に指定すれば良い。