JavaのSSLSocketを使う

# はじめに
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は不要。

コードとしては以下のようになる。

```java
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 を作る。

コードとしては以下の通り。

```java
            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 <出力ファイル名> 
```

これを実行するとパスフレーズを設定するため、２回入力する必要がある。

# 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にするとオプション指定ができる。
指定できるオプションは[ここ](https://colinpaice.blog/2020/04/05/using-java-djavax-net-debug-to-examine-data-flows-including-tls/)参照。

# TLSのバージョンについて
OpenJDK 17 では、デフォルトで TLS 1.2, 1.3 のみ有効になっている。
SSLServerSocket.setEnabledProtocolsで指定可能。例えば、以下のようにすると TLS 1.3 のみしか受け付けなくなる。

```java
    SSLServerSocket sslsvr = (SSLServerSocket)ssf.createServerSocket(443);
    sslsvr.setEnabledProtocols(new String[] { "TLSv1.3" });
```

使えるプロトコルの確認は getEnabledProtocols で可能。

同じように Cipher Suites の変更は setEnabledCipherSuites で可能、確認は getEnabledCipherSuites なので、getEnabledCipherSuites してから、使いたくないものを削って setEnabledCipherSuites に指定すれば良い。