OpenSSLを使用し、証明書作成からSSLSocket通信までまとめてみました
【目的】
前回作成したHTTPサーバをHTTPSサーバにも切り替え可能な状態にしてみようと思いました。
Javaでフレームワーク(Httpサーバ)を作成してみる
【概要】
①OpenSSLでキー作成~サーバ証明書の作成
②Javaで上記で作成した証明書を利用し、SSL通信の確認をする
③ブラウザとサーバの通信を確認する
目次
1. OpenSSLを使用しキー作成~サーバ証明書の作成を行う
2. JavaでSSLSocket通信を構築する
3. クライアント認証を実装する
4. SSLSessionの状態を確認する
5. 前回作成したフレームワークをHTTPS通信可能な状態にする
6. 参考
1. OpenSSLを使用しキー作成~サーバ証明書の作成を行う
今回、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
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
subjectAltName = DNS:localhost, IP:127.0.0.1
証明書署名要求ファイル生成時に指定いている
CNはURL、FQDNを設定する為、証明書発行時の-extfileで指定のファイルに設定したDNSと同じ内容を設定します。一度、サーバ証明書を確認してみます。
openssl x509 -noout -text -in server.crt
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の登録が必要
早速クライアントから実装していきます。(今回試験動作の為、クライアントとサーバが同プロセスとなります。本来別プロセスにするべきなのでしょうが。。)
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();
}
}
}
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を使用し信頼性を確立します。
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とすることにします。
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
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
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に格納しました。
サーバ側でクライアント認証を必須とする設定を行います
public class ServerSSL extends Thread {
//省略
private void setting() throws Exception {
//省略
svrSock_ = (SSLServerSocket)ssf.createServerSocket(9999);
//クライアン認証を必須とする
svrSock_.setNeedClientAuth(true);
}
}
サーバサイドでクライアント証明書を認証する為、トラストストアにca2.p12を追加し、SSLContextに登録します
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に登録します
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のハンドシェイクで決定した内容など確認してみます。
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();
}
}
}
折角なんで暗号化されたのをデコードしました
openssl pkcs8 -in server.key -topk8 -nocrypt -outform DER -out server.pk8
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通信が可能となりました。
6. 参考
参考にさせて頂きました記事を記載致しました。
Wireshark で TLS ハンドシェイクの流れを見てみる
図解 X.509 証明書