OpenSSLとSSLSocketを使用しHTTPS通信を構築してみる

### OpenSSLを使用し、証明書作成からSSLSocket通信までまとめてみました
#####  【目的】
前回作成したHTTPサーバをHTTPSサーバにも切り替え可能な状態にしてみようと思いました。
[Javaでフレームワーク(Httpサーバ)を作成してみる](https://qiita.com/yuki-yanagawa/items/73482aa142fa817bf089)
##### 【概要】
①OpenSSLでキー作成～サーバ証明書の作成
②Javaで上記で作成した証明書を利用し、SSL通信の確認をする
③ブラウザとサーバの通信を確認する

## 目次
[1. OpenSSLを使用しキー作成～サーバ証明書の作成を行う](#1-opensslを使用しキー作成サーバ証明書の作成を行う)
[2. JavaでSSLSocket通信を構築する](#2-javaでsslsocket通信を構築する)
[3. クライアント認証を実装する](#3-クライアント認証を実装する)
[4. SSLSessionの状態を確認する](#4-sslsessionの状態を確認する)
[5. 前回作成したフレームワークをHTTPS通信可能な状態にする](#5-前回作成したフレームワークをhttps通信可能な状態にする)
[6. 参考](#6-参考)




## 1. OpenSSLを使用しキー作成～サーバ証明書の作成を行う
今回、CAを作成し自作したCAがサーバ証明書を発行する流れにしたいと思います。
```sh: CA作成
openssl genrsa -out ca.key 2048
#証明書署名要求ファイル生成
openssl req -new -key ca.key -subj "/C=JP/ST=Miyagi/O=TestOrg/CN=myca" -out ca.csr
#自己署名
openssl x509 -days 365 -req -extfile subjectnamesforca.txt -signkey ca.key -in ca.csr -out ca.crt
```
```text: subjectnamesforca.txt
subjectAltName = DNS:myca, IP:127.0.0.1
```
```sh: サーバ証明書作成
openssl genrsa -out server.key 2048
#証明書署名要求ファイル生成
openssl req -new -key server.key -subj "/C=JP/ST=Miyagi/O=TestServerOrg/CN=localhost" -out server.csr
#CAにサーバ証明書を発行してもらう
openssl x509 -days 365 -req -CA ca.crt -CAkey ca.key -CAcreateserial -extfile subjectnamesforserver.txt -in server.csr -out server.crt
```
```text: subjectnamesforserver.txt
subjectAltName = DNS:localhost, IP:127.0.0.1
```
証明書署名要求ファイル生成時に指定いている
CNはURL、FQDNを設定する為、証明書発行時の-extfileで指定のファイルに設定したDNSと同じ内容を設定します。一度、サーバ証明書を確認してみます。
```sh: サーバ証明書の確認
openssl x509 -noout -text -in server.crt
```
発行者の名称が作成したCAであることを確認できました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3530697/3d8b24c8-da05-830a-1059-3987a74d793e.png)

KeyStoreに証明書ファイルをインポートする方法もありますが、Javaで証明書読み込み使用する形にする為、ファイルフォーマットをpkcs12に変換します。
```sh: ファイルフォーマットの変換
openssl pkcs12 -export -in ca.crt -inkey ca.key -out ca.p12
openssl pkcs12 -export -in server.crt -inkey server.key -out server.p12
```
パスワードの入力が求められます。今回はCAを「CAPASSWORD」、SERVERを「SVRPASSWORD」と致しました。


次に作成した証明書を使用しJavaでSSLSocketを使用した通信を確認していこうと思います。

## 2. JavaでSSLSocket通信を構築する
こちらで作成したプログラムは以下になります。

https://github.com/yuki-yanagawa/SurveySSLSocket

ポイントは以下のように思っています。
・SSL(TLS)のハンドシェイクはクライアントをきっかけに行う
・SSLSessionにハンドシェイクで決定した内容が設定されている
・HandshakeCompletedListenerを実装すれば、ハンドシェイクの完了後イベントの確認ができる。完了後の処理が実装できる
・サーバ証明書認証する場合(クライアントサイド)、SSLContextにTrustManagerの登録が必要、認証される場合(サーバサイド)、SSLContextにKeyManagerの登録が必要
早速クライアントから実装していきます。（今回試験動作の為、クライアントとサーバが同プロセスとなります。本来別プロセスにするべきなのでしょうが。。）
```java: Main.java
public class Main {
	public static void main(String[] args) {
		try {
			ServerSSL server = new ServerSSL();
			server.start();
			Thread.sleep(1000);
			ClientSSL client = new ClientSSL();
			client.start();
		} catch(Exception e) {
			e.printStackTrace();
		}
	}
}
```

```java: ClientSSL.java
public class ClientSSL extends Thread implements HandshakeCompletedListener {
	private SSLSocket socket_;
	public ClientSSL() throws Exception {
		setting();
	}
	
	private void setting() throws Exception {
        //certdirforclient\ca.p12を読み込む
		FileInputStream fis = new FileInputStream("certdirforclient/ca.p12");
        //キーストア(トラストストア)を生成する
		KeyStore trustStore = KeyStore.getInstance("pkcs12");
		trustStore.load(fis, "CAPASSWORD".toCharArray());
		//生成したキーストア(トラストストア)をTrustManagerFactoryに登録する
        //init処理でKeyStoreを基にTrustManagerオブジェクトが生成される
		TrustManagerFactory tmf = TrustManagerFactory.getInstance("SunX509");
		tmf.init(trustStore);
		
		SSLContext sc = SSLContext.getInstance("TLS");
        //SSLContextにTrustManagerを登録する
		sc.init(null, tmf.getTrustManagers(), null);
		
		SSLSocketFactory sf = sc.getSocketFactory();
		socket_ = (SSLSocket)sf.createSocket();
		socket_.addHandshakeCompletedListener(this);
	}

	@Override
	public void run() {
		connect();
	}
	
	private void connect() {
		try {
			socket_.connect(new InetSocketAddress("127.0.0.1", 9999));
            //SSLのハンドシェイク
			socket_.startHandshake();
		} catch(IOException e) {
			e.printStackTrace();
		}
	}

	public void handshakeCompleted(HandshakeCompletedEvent event) {
		try {
			OutputStream os = socket_.getOutputStream();
			os.write("connection OK From Client!!!".getBytes());
			os.flush();
			os.close();
			socket_.close();
		} catch(IOException e) {
			e.printStackTrace();
		}
	}
}
```
>(クライアントサイド処理概要)
1.certdirforclientフォルダ配下に作成したCA証明書を格納する
2.certdirforclient\ca.p12を読み込む
3.KeyStore(TrustStore)を生成する
4.生成したKeyStoreを基にX509TrustManagerオブジェクトが生成される
5.SSLContextにX509TrustManagerを登録する
6.サーバに接続後(3ウェイハンドシェイクの後)、SSLのハンドシェイクを行います
7.SSLのハンドシェイクが完了後、handshakeCompletedメソッドに処理がうつります
上記内容により、サーバから受け取った証明書の認証をチェックする際にca.p12を使用し信頼性を確立します。


```java: ServerSSL.java
public class ServerSSL extends Thread {
	private SSLServerSocket svrSock_;
	public ServerSSL() throws Exception {
		setting();
	}
	private void setting() throws Exception {
        //キーストアを生成する
		FileInputStream fis = new FileInputStream("certdirforserver/server.p12");
		KeyStore keyStore = KeyStore.getInstance("pkcs12");
		keyStore.load(fis, "SVRPASSWORD".toCharArray());

        //生成したKeyStoreをKeyManagerFactoryに登録する
        //init処理でKeyStoreを基にKeyManagerオブジェクトが生成される
		KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509");
		kmf.init(keyStore, "SVRPASSWORD".toCharArray());
		
		SSLContext sc = SSLContext.getInstance("TLS");
        //SSLContextにKeyManagerを登録する
		sc.init(kmf.getKeyManagers(), null, null);
		
		SSLServerSocketFactory ssf = sc.getServerSocketFactory();
		svrSock_ = (SSLServerSocket)ssf.createServerSocket(9999);
	}
	@Override
	public void run() {
		accept();
	}
	
	private void accept() {
		try {
			while(true) {
				SSLSocket socket = (SSLSocket)svrSock_.accept();
				try {
					byte[] buf = new byte[512];
					InputStream is = socket.getInputStream();
					is.read(buf);
					System.out.println(new String(buf));
					is.close();
					socket.close();
				} catch(IOException e) {
					e.printStackTrace();
				}
			}
		} catch(IOException e) {
			e.printStackTrace();
		}
	}
}
```
>(サーバサイド処理概要)
1.certdirforserverフォルダ配下に作成したServer証明書を格納する
2.certdirforclient\server.p12を読み込む
3.KeyStoreを生成する
4.生成したKeyStoreを基にKeyManager(X509ExtendedKeyManager)オブジェクトが生成される
5.SSLContextにKeyManagerを登録する
上記内容により、SSLハンドシェイク時に登録したサーバ証明書をクライアントへ送りクライアントで認証を行います
上記内容でSSLSocketを使用した通信が実装できました。

## 3. クライアント認証を実装する
クライアント証明書をOpenSSL使用し作成します。手順はサーバ証明書と同手順で行おうと思います。クライアントサーバ証明書を発行するCAはサーバ証明書を発行したCAと別のCAとすることにします。
```sh: CA作成
openssl genrsa -out ca2.key 2048
#証明書署名要求ファイル生成
openssl req -new -key ca2.key -subj "/C=JP/ST=Miyagi/O=TestOrg/CN=myca2" -out ca2.csr
#自己署名
openssl x509 -days 365 -req -extfile subjectnamesforca2.txt -signkey ca2.key -in ca2.csr -out ca2.crt
```
```text: subjectnamesforca2.txt
subjectAltName = DNS:myca2, IP:127.0.0.1
```
```sh: クライアント証明書作成
openssl genrsa -out client.key 2048
#証明書署名要求ファイル生成
openssl req -new -key client.key -subj "/C=JP/ST=Miyagi/O=TestClientOrg/CN=myClient" -out client.csr
#CAにクライアント証明書を発行してもらう
openssl x509 -days 365 -req -CA ca2.crt -CAkey ca2.key -CAcreateserial -extfile subjectnamesforclient.txt -in client.csr -out client.crt
```
```text: subjectnamesforclient.txt
subjectAltName = DNS:myClient, IP:127.0.0.1
```
```sh: ファイルフォーマットの変換
openssl pkcs12 -export -in ca2.crt -inkey ca2.key -out ca2.p12
openssl pkcs12 -export -in client.crt -inkey client.key -out client.p12
```
パスワードはCA2を「CA2PASSWORD」、CLIENTを「CLIPASSWORD」と致しました。
ca2.p12はcertdirforserver、client.p12はcertdirforserverに格納しました。
##### サーバ側でクライアント認証を必須とする設定を行います
```java: ServerSSL.java
public class ServerSSL extends Thread {
	//省略
	private void setting() throws Exception {
        //省略
		svrSock_ = (SSLServerSocket)ssf.createServerSocket(9999);
        //クライアン認証を必須とする
        svrSock_.setNeedClientAuth(true);
	}
}
```
##### サーバサイドでクライアント証明書を認証する為、トラストストアにca2.p12を追加し、SSLContextに登録します
```java: ServerSSL.java
public class ServerSSL extends Thread {
	private SSLServerSocket svrSock_;
	public ServerSSL() throws Exception {
		setting();
	}
	private void setting() throws Exception {
		FileInputStream fis = new FileInputStream("certdirforserver/server.p12");
		KeyStore keyStore = KeyStore.getInstance("pkcs12");
		keyStore.load(fis, "SVRPASSWORD".toCharArray());
		
		KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509");
		kmf.init(keyStore, "SVRPASSWORD".toCharArray());

        //今回追加した箇所 START
        //クライアント認証の為、クライアント証明書を発行したca2の証明書をトラストストアに登録する
		FileInputStream fis2 = new FileInputStream("certdirforserver/ca2.p12");
		KeyStore trustStore = KeyStore.getInstance("pkcs12");
		trustStore.load(fis2, "CA2PASSWORD".toCharArray());
		
		TrustManagerFactory tmf = TrustManagerFactory.getInstance("SunX509");
		tmf.init(trustStore);
		//今回追加した箇所 END

		SSLContext sc = SSLContext.getInstance("TLS");
        //SSLContextにtrustManagerを登録する
		sc.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null);
		
		SSLServerSocketFactory ssf = sc.getServerSocketFactory();
		svrSock_ = (SSLServerSocket)ssf.createServerSocket(9999);
		svrSock_.setNeedClientAuth(true);
	}
	@Override
	public void run() {
		accept();
	}
	
	private void accept() {
		try {
			while(true) {
				SSLSocket socket = (SSLSocket)svrSock_.accept();
				try {
					byte[] buf = new byte[512];
					InputStream is = socket.getInputStream();
					is.read(buf);
					System.out.println(new String(buf));
					is.close();
					socket.close();
				} catch(IOException e) {
					e.printStackTrace();
				}
			}
		} catch(IOException e) {
			e.printStackTrace();
		}
	}
}
```
##### クライアントサイドでクライアント証明書をキーストアにclient.p12を追加し、SSLContextに登録します
```java: ClientSSL.java
public class ClientSSL extends Thread implements HandshakeCompletedListener {
	private SSLSocket socket_;
	public ClientSSL() throws Exception {
		setting();
	}
	
	private void setting() throws Exception {
		FileInputStream fis = new FileInputStream("certdirforclient/ca.p12");
		KeyStore trustStore = KeyStore.getInstance("pkcs12");
		trustStore.load(fis, "CAPASSWORD".toCharArray());
		
		TrustManagerFactory tmf = TrustManagerFactory.getInstance("SunX509");
		tmf.init(trustStore);

        //今回追加した箇所 START
        //クライアント認証の為、クライアント証明書をキーストアに登録する
		FileInputStream fis2 = new FileInputStream("certdirforclient/client.p12");
		KeyStore keyStore = KeyStore.getInstance("pkcs12");
		keyStore.load(fis2, "CLIPASSWORD".toCharArray());
		
		KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509");
		kmf.init(keyStore, "CLIPASSWORD".toCharArray());
        //今回追加した箇所 END
		
		SSLContext sc = SSLContext.getInstance("TLS");
        //SSLContextにkeyManagerを登録する
		sc.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null);
		
		SSLSocketFactory sf = sc.getSocketFactory();
		socket_ = (SSLSocket)sf.createSocket();
		socket_.addHandshakeCompletedListener(this);
		
	}

	@Override
	public void run() {
		connect();
	}
	
	private void connect() {
		try {
			socket_.connect(new InetSocketAddress("localhost", 9999));
			socket_.startHandshake();
		} catch(IOException e) {
			e.printStackTrace();
		}
	}

	public void handshakeCompleted(HandshakeCompletedEvent event) {
		try {
			OutputStream os = socket_.getOutputStream();
			os.write("connection OK From Client!!!".getBytes());
			os.flush();
			os.close();
			socket_.close();
		} catch(IOException e) {
			e.printStackTrace();
		}
	}
}
```

## 4. SSLSessionの状態を確認する
クライアントのコードを改変し、SSLのハンドシェイクで決定した内容など確認してみます。
```java: ClientSSL.java
public class ClientSSL extends Thread implements HandshakeCompletedListener {
	//省略
	public void handshakeCompleted(HandshakeCompletedEvent event) {
		try {
			SSLSession session = socket_.getSession();
			//クライアントサーバ間で決定した暗号の確認
			System.out.println(session.getCipherSuite());
			//サーバから送られてきた証明書を確認します
			X509Certificate[] x509Certificates = session.getPeerCertificateChain();
			for(X509Certificate x509Certificate : x509Certificates) {
				System.out.println(x509Certificate);
				System.out.println(x509Certificate.getSubjectDN().getName());
			}
            //証明書から公開鍵を取得し暗号化してみます。。
			PublicKey publicKey = x509Certificates[0].getPublicKey();
			Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
			cipher.init(Cipher.ENCRYPT_MODE, publicKey);
			OutputStream os = socket_.getOutputStream();
			os.write(cipher.doFinal("connection OK From Client!!!".getBytes()));
			os.flush();
			os.close();
			socket_.close();
		} catch(Exception e) {
			e.printStackTrace();
		}
	}
}
```
折角なんで暗号化されたのをデコードしました
```sh: server.keyの変換
openssl pkcs8 -in server.key -topk8 -nocrypt -outform DER -out server.pk8
```
```java: ServerSSL.java
public class ServerSSL extends Thread {
	//省略
	private void accept() {
		try {
			while(true) {
				SSLSocket socket = (SSLSocket)svrSock_.accept();
				try {
					byte[] buf = new byte[256];
					InputStream is = socket.getInputStream();
					is.read(buf);

					FileInputStream fiskey = new FileInputStream("certdirforserver/server.pk8");
					byte[] bufkey = new byte[2048];
					fiskey.read(bufkey);
					PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(bufkey);
					KeyFactory kf = KeyFactory.getInstance("RSA");
					PrivateKey privateKey = kf.generatePrivate(keySpec);
					Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
					cipher.init(Cipher.DECRYPT_MODE, privateKey);
					byte[] data = cipher.doFinal(buf);
					
					
					System.out.println(new String(data));
					is.close();
					socket.close();
				} catch(IOException e) {
					e.printStackTrace();
				}
			}
		} catch(Exception e) {
			e.printStackTrace();
		}
	}
}
```

## 5. 前回作成したフレームワークをHTTPS通信可能な状態にする
こちらが対応後のコードになります。

https://github.com/yuki-yanagawa/FrameWorkTest


基本的には、上記のSSLSocket通信の流れと変更ありませんが、自作のCAをブラウザに登録します。
Chromeの場合、設定 -> プライバシーとセキュリティ -> セキュリティ -> デバイス証明書の管理 -> 信頼されたルート証明書機関タブを押し、インポートをクリック、その後手順に従い、自作のCA証明書を登録します。これにより、https通信が可能となりました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3530697/9739b8ad-b1e0-56b9-42b5-7c668507a3cb.png)


## 6. 参考
参考にさせて頂きました記事を記載致しました。
[Wireshark で TLS ハンドシェイクの流れを見てみる](https://qiita.com/Shinya-Yamaguchi/items/becd042f641362c432bc)
[図解 X.509 証明書](https://qiita.com/TakahikoKawasaki/items/4c35ac38c52978805c69#4-%E8%87%AA%E5%B7%B1%E7%BD%B2%E5%90%8D%E8%A8%BC%E6%98%8E%E6%9B%B8%E4%BD%9C%E6%88%90)