0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

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

Posted at

OpenSSLを使用し、証明書作成からSSLSocket通信までまとめてみました

【目的】

前回作成したHTTPサーバをHTTPSサーバにも切り替え可能な状態にしてみようと思いました。
Javaでフレームワーク(Httpサーバ)を作成してみる

【概要】

①OpenSSLでキー作成~サーバ証明書の作成
②Javaで上記で作成した証明書を利用し、SSL通信の確認をする
③ブラウザとサーバの通信を確認する

目次

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

1. OpenSSLを使用しキー作成~サーバ証明書の作成を行う

今回、CAを作成し自作したCAがサーバ証明書を発行する流れにしたいと思います。

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
subjectnamesforca.txt
subjectAltName = DNS:myca, IP:127.0.0.1
サーバ証明書作成
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
subjectnamesforserver.txt
subjectAltName = DNS:localhost, IP:127.0.0.1

証明書署名要求ファイル生成時に指定いている
CNはURL、FQDNを設定する為、証明書発行時の-extfileで指定のファイルに設定したDNSと同じ内容を設定します。一度、サーバ証明書を確認してみます。

サーバ証明書の確認
openssl x509 -noout -text -in server.crt

発行者の名称が作成したCAであることを確認できました。
image.png

KeyStoreに証明書ファイルをインポートする方法もありますが、Javaで証明書読み込み使用する形にする為、ファイルフォーマットをpkcs12に変換します。

ファイルフォーマットの変換
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通信を構築する

こちらで作成したプログラムは以下になります。

ポイントは以下のように思っています。
・SSL(TLS)のハンドシェイクはクライアントをきっかけに行う
・SSLSessionにハンドシェイクで決定した内容が設定されている
・HandshakeCompletedListenerを実装すれば、ハンドシェイクの完了後イベントの確認ができる。完了後の処理が実装できる
・サーバ証明書認証する場合(クライアントサイド)、SSLContextにTrustManagerの登録が必要、認証される場合(サーバサイド)、SSLContextにKeyManagerの登録が必要
早速クライアントから実装していきます。(今回試験動作の為、クライアントとサーバが同プロセスとなります。本来別プロセスにするべきなのでしょうが。。)

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();
		}
	}
}
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を使用し信頼性を確立します。

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とすることにします。

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
subjectnamesforca2.txt
subjectAltName = DNS:myca2, IP:127.0.0.1
クライアント証明書作成
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
subjectnamesforclient.txt
subjectAltName = DNS:myClient, IP:127.0.0.1
ファイルフォーマットの変換
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に格納しました。

サーバ側でクライアント認証を必須とする設定を行います
ServerSSL.java
public class ServerSSL extends Thread {
	//省略
	private void setting() throws Exception {
        //省略
		svrSock_ = (SSLServerSocket)ssf.createServerSocket(9999);
        //クライアン認証を必須とする
        svrSock_.setNeedClientAuth(true);
	}
}
サーバサイドでクライアント証明書を認証する為、トラストストアにca2.p12を追加し、SSLContextに登録します
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に登録します
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のハンドシェイクで決定した内容など確認してみます。

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();
		}
	}
}

折角なんで暗号化されたのをデコードしました

server.keyの変換
openssl pkcs8 -in server.key -topk8 -nocrypt -outform DER -out server.pk8
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通信可能な状態にする

こちらが対応後のコードになります。

基本的には、上記のSSLSocket通信の流れと変更ありませんが、自作のCAをブラウザに登録します。
Chromeの場合、設定 -> プライバシーとセキュリティ -> セキュリティ -> デバイス証明書の管理 -> 信頼されたルート証明書機関タブを押し、インポートをクリック、その後手順に従い、自作のCA証明書を登録します。これにより、https通信が可能となりました。
image.png

6. 参考

参考にさせて頂きました記事を記載致しました。
Wireshark で TLS ハンドシェイクの流れを見てみる
図解 X.509 証明書

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?