BouncyCastleを使ってJava6でTLS1.2でのHTTPS通信を行う
どうも。煉獄乙女です。おにぎりはラップよりアルミホイル派です。
しっとり貼りついてパリパリ感0の味付け海苔が大好きです。
ふりかけも先がけ派です。魁!
今回はBouncyCastleというライブラリを使用して、
無理やりJava6でTLS1.2を用いたHTTPS通信やったんで~っていう話です。
余計なことごちゃごちゃ書いてるので導入方法だけ見たいんや!って方は目次から飛ぶの推奨です。
目的
BouncyCastleを使用して、JDK1.6でTLS1.2を使用する。
※Java6以下ではそのまま指定するだけではTLS1.2が使えないのだ!
Java7はデフォルトがver1です!が1.2は指定すれば使えます!
「じゃあそもそもバージョンアップしろよ!今時6とか流行んね~から!」とお思いのみなさん。
我がシステムでは一部JDK1.5.0が現役ですことよ。オ~ホッホ!あるある。ですよね?
Java8 Silverの勉強の時ものすごいカルチャーショック受けました(小声)
Javaアップデートするのはいいけど強化テストが大変っつ~わけで、
アップデートの影響確認に人員&工数を割けるその日まで、
「TLS1.2しか使えないよ~~ん」って期限直前に
赤紙通達してきた事業者に対応しなければならないのだ!(使命感)
そんな使命感に燃える我ら企業戦士の味方がBouncyCastle。
Java5でも6でもTLS1.2が使えちゃうのであーる。URであーる。
(ところでどん兵衛のCMの吉岡里帆ぎつねめっちゃかわいいですよね)
BouncyCastleってなんだ
暗号化ライブラリです!以上!
………
……まぁ間違ってはない
※Bouncy Castleとは、暗号化関連の機能を提供してくれるライブラリです。
今回説明するTLS1.2でのHTTP通信だけでなく、
RSA鍵や電子証明書やPKCS12ファイルを生成してくれたり、
読み込んでくれたり、書き込んでくれたりもします。
その辺は日本語でリファレンス転がってるからぐぐってくれよな!
経緯
私今から怖い話しまっせでおまっせ。
実はね、TLS1.2がJava6で使えないとかそもそも知らなかったんですよ私。
最初はサーバからcurlでURL叩いて接続確認してぬか喜びしてたわけなんですけどね、
API介してみたらSSLHandshakeExceptionで証明書エラーが出たんですね。
※SSLHandshakeException: Received fatal alert: handshake_failure
焦って証明書追加しても直らなくて、そこで調べてね、やっと
Java6でTLS1.2がデフォで使えない事を知ったんですよ!!!(キャーーー)
※なおデフォで使えないというのは、後の有償版のアップデートで辛うじて使えるっぽいです。
ほなどうすんねんて~、調べてみたんですよ~、
もうこれはJavaアップデート不可避なのか?と。
そしたらどうやらBouncyCastleとかいうやつを使えばいけるらしい。
まじかよ!!すげえ!!と思ってリファレンス調べたら、
案の定英語でしか書いてないんですね。
でまぁ、このご時勢、TLS1.2に完全移行する事業者さんも多いでしょうし、
(TLS1.2は2018年の期日までに対応しなければならないとかならなくないとか)
アップデートせずに遺産のように古いJavaを使ってる人もいるでしょうから、
ちょっとまとめようと思いましてん。技術ブログですしね。
導入方法
やっと本題です。
1.ライブラリ追加
まずライブラリ追加します。
適用システムはMavenプロジェクトのため、pom.xmlに以下の記述を追加。
<!-- TLS1.2通信用 -->
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk15on</artifactId>
<version>1.54</version>
</dependency>
MavenじゃないプロジェクトならWEB-INF/libなどのライブラリまとめてるところに
DLしてきたjarファイルを突っ込みましょう。
バージョンとかもろもろはここ確認してくれよな!DLも以下からできるでな!http://www.bouncycastle.org/latest_releases.html
2.TLSSocketConnectionFactoryクラスを作成
TLS1.2を用いて通信を行うために、SSLSocketFactoryを継承したTLSSocketConnectionFactoryクラスを作成します。
証明書エラーのコメント部分の下一行を削除すれば
証明書の登録なしで全て通りますのでご注意を!
その他エラー部分は必要に応じて改変していただければと思います。
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.UnknownHostException;
import java.security.KeyStore;
import java.security.Principal;
import java.security.SecureRandom;
import java.security.Security;
import java.security.cert.CertificateException;
import java.security.cert.CertificateExpiredException;
import java.security.cert.CertificateFactory;
import java.util.Hashtable;
import java.util.LinkedList;
import java.util.List;
import javax.net.ssl.HandshakeCompletedListener;
import javax.net.ssl.SSLPeerUnverifiedException;
import javax.net.ssl.SSLSession;
import javax.net.ssl.SSLSessionContext;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
import javax.security.cert.X509Certificate;
import org.bouncycastle.crypto.tls.Certificate;
import org.bouncycastle.crypto.tls.CertificateRequest;
import org.bouncycastle.crypto.tls.DefaultTlsClient;
import org.bouncycastle.crypto.tls.ExtensionType;
import org.bouncycastle.crypto.tls.TlsAuthentication;
import org.bouncycastle.crypto.tls.TlsClientProtocol;
import org.bouncycastle.crypto.tls.TlsCredentials;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
public class TLSSocketConnectionFactory extends SSLSocketFactory {
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
//Adding Custom BouncyCastleProvider
///////////////////////////////////////////////////////////////////////////////////////////////////////////////
static {
if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) {
Security.addProvider(new BouncyCastleProvider());
}
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
//SECURE RANDOM
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
private SecureRandom _secureRandom = new SecureRandom();
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
//Adding Custom BouncyCastleProvider
///////////////////////////////////////////////////////////////////////////////////////////////////////////////
@Override
public Socket createSocket(Socket socket, final String host, int port, boolean arg3)
throws IOException {
if (socket == null) {
socket = new Socket();
}
if (!socket.isConnected()) {
socket.connect(new InetSocketAddress(host, port));
}
final TlsClientProtocol tlsClientProtocol = new TlsClientProtocol(socket.getInputStream(),
socket.getOutputStream(), _secureRandom);
return _createSSLSocket(host, tlsClientProtocol);
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//SOCKET FACTORY METHODS
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
@Override
public String[] getDefaultCipherSuites() {
return null;
}
@Override
public String[] getSupportedCipherSuites() {
return null;
}
@Override
public Socket createSocket(String host,
int port) throws IOException, UnknownHostException {
return null;
}
@Override
public Socket createSocket(InetAddress host,
int port) throws IOException {
return null;
}
@Override
public Socket createSocket(String host,
int port,
InetAddress localHost,
int localPort) throws IOException, UnknownHostException {
return null;
}
@Override
public Socket createSocket(InetAddress address,
int port,
InetAddress localAddress,
int localPort) throws IOException {
return null;
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//SOCKET CREATION
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
private SSLSocket _createSSLSocket(final String host, final TlsClientProtocol tlsClientProtocol) {
return new SSLSocket() {
private java.security.cert.Certificate[] peertCerts;
@Override
public InputStream getInputStream() throws IOException {
return tlsClientProtocol.getInputStream();
}
@Override
public OutputStream getOutputStream() throws IOException {
return tlsClientProtocol.getOutputStream();
}
@Override
public synchronized void close() throws IOException {
tlsClientProtocol.close();
}
@Override
public void addHandshakeCompletedListener(HandshakeCompletedListener arg0) {
}
@Override
public boolean getEnableSessionCreation() {
return false;
}
@Override
public String[] getEnabledCipherSuites() {
return null;
}
@Override
public String[] getEnabledProtocols() {
return null;
}
@Override
public boolean getNeedClientAuth() {
return false;
}
@Override
public SSLSession getSession() {
return new SSLSession() {
@Override
public int getApplicationBufferSize() {
return 0;
}
@Override
public String getCipherSuite() {
throw new UnsupportedOperationException();
}
@Override
public long getCreationTime() {
throw new UnsupportedOperationException();
}
@Override
public byte[] getId() {
throw new UnsupportedOperationException();
}
@Override
public long getLastAccessedTime() {
throw new UnsupportedOperationException();
}
@Override
public java.security.cert.Certificate[] getLocalCertificates() {
throw new UnsupportedOperationException();
}
@Override
public Principal getLocalPrincipal() {
throw new UnsupportedOperationException();
}
@Override
public int getPacketBufferSize() {
throw new UnsupportedOperationException();
}
@Override
public X509Certificate[] getPeerCertificateChain()
throws SSLPeerUnverifiedException {
return null;
}
@Override
public java.security.cert.Certificate[] getPeerCertificates() throws SSLPeerUnverifiedException {
return peertCerts;
}
@Override
public String getPeerHost() {
throw new UnsupportedOperationException();
}
@Override
public int getPeerPort() {
return 0;
}
@Override
public Principal getPeerPrincipal() throws SSLPeerUnverifiedException {
return null;
//throw new UnsupportedOperationException();
}
@Override
public String getProtocol() {
throw new UnsupportedOperationException();
}
@Override
public SSLSessionContext getSessionContext() {
throw new UnsupportedOperationException();
}
@Override
public Object getValue(String arg0) {
throw new UnsupportedOperationException();
}
@Override
public String[] getValueNames() {
throw new UnsupportedOperationException();
}
@Override
public void invalidate() {
throw new UnsupportedOperationException();
}
@Override
public boolean isValid() {
throw new UnsupportedOperationException();
}
@Override
public void putValue(String arg0, Object arg1) {
throw new UnsupportedOperationException();
}
@Override
public void removeValue(String arg0) {
throw new UnsupportedOperationException();
}
};
}
@Override
public String[] getSupportedProtocols() {
return null;
}
@Override
public boolean getUseClientMode() {
return false;
}
@Override
public boolean getWantClientAuth() {
return false;
}
@Override
public void removeHandshakeCompletedListener(HandshakeCompletedListener arg0) {
}
@Override
public void setEnableSessionCreation(boolean arg0) {
}
@Override
public void setEnabledCipherSuites(String[] arg0) {
}
@Override
public void setEnabledProtocols(String[] arg0) {
}
@Override
public void setNeedClientAuth(boolean arg0) {
}
@Override
public void setUseClientMode(boolean arg0) {
}
@Override
public void setWantClientAuth(boolean arg0) {
}
@Override
public String[] getSupportedCipherSuites() {
return null;
}
@Override
public void startHandshake() throws IOException {
tlsClientProtocol.connect(new DefaultTlsClient() {
@SuppressWarnings("unchecked")
@Override
public Hashtable<Integer, byte[]> getClientExtensions() throws IOException {
Hashtable<Integer, byte[]> clientExtensions = super.getClientExtensions();
if (clientExtensions == null) {
clientExtensions = new Hashtable<Integer, byte[]>();
}
//Add host_name
byte[] host_name = host.getBytes();
final ByteArrayOutputStream baos = new ByteArrayOutputStream();
final DataOutputStream dos = new DataOutputStream(baos);
dos.writeShort(host_name.length + 3);
dos.writeByte(0); //
dos.writeShort(host_name.length);
dos.write(host_name);
dos.close();
clientExtensions.put(ExtensionType.server_name, baos.toByteArray());
return clientExtensions;
}
@Override
public TlsAuthentication getAuthentication()
throws IOException {
return new TlsAuthentication() {
@Override
public void notifyServerCertificate(Certificate serverCertificate) throws IOException {
try {
KeyStore ks = _loadKeyStore();
CertificateFactory cf = CertificateFactory.getInstance("X.509");
Listcerts = new LinkedList ();
boolean trustedCertificate = false;
for (org.bouncycastle.asn1.x509.Certificate c : serverCertificate
.getCertificateList()) {
java.security.cert.Certificate cert = cf
.generateCertificate(new ByteArrayInputStream(c.getEncoded()));
certs.add(cert);
String alias = ks.getCertificateAlias(cert);
if (alias != null) {
if (cert instanceof java.security.cert.X509Certificate) {
try {
((java.security.cert.X509Certificate) cert).checkValidity();
trustedCertificate = true;
} catch (CertificateExpiredException cee) {
// 登録した証明書が期限切れ時の処理
throw new CertificateExpiredException("this cert is expired : " + alias );
}
}
} else {
// 登録されている証明書のエイリアスがNULL時の処理
throw new CertificateException("cert alias is null : " + alias );
}
}
if (!trustedCertificate) {
// 証明書エラー時の処理
throw new CertificateException("Unknown cert " + serverCertificate);
}
peertCerts = certs.toArray(new java.security.cert.Certificate[0]);
} catch (Exception ex) {
ex.printStackTrace();
throw new IOException(ex);
}
}
@Override
public TlsCredentials getClientCredentials(CertificateRequest arg0)
throws IOException {
return null;
}
/**
* Private method to load keyStore with system or default properties.
* @return
* @throws Exception
*/
private KeyStore _loadKeyStore() throws Exception {
FileInputStream trustStoreFis = null;
try {
String sysTrustStore = null;
File trustStoreFile = null;
KeyStore localKeyStore = null;
sysTrustStore = System.getProperty("javax.net.ssl.trustStore");
String javaHome;
if (!"NONE".equals(sysTrustStore)) {
if (sysTrustStore != null) {
trustStoreFile = new File(sysTrustStore);
trustStoreFis = _getFileInputStream(trustStoreFile);
} else {
javaHome = System.getProperty("java.home");
trustStoreFile = new File(javaHome + File.separator + "lib"
+ File.separator + "security" + File.separator + "jssecacerts");
if ((trustStoreFis = _getFileInputStream(trustStoreFile)) == null) {
trustStoreFile = new File(javaHome + File.separator + "lib"
+ File.separator + "security" + File.separator + "cacerts");
trustStoreFis = _getFileInputStream(trustStoreFile);
}
}
if (trustStoreFis != null) {
sysTrustStore = trustStoreFile.getPath();
} else {
sysTrustStore = "No File Available, using empty keystore.";
}
}
String trustStoreType = System.getProperty("javax.net.ssl.trustStoreType") != null ? System
.getProperty("javax.net.ssl.trustStoreType") : KeyStore.getDefaultType();
String trustStoreProvider = System.getProperty("javax.net.ssl.trustStoreProvider") != null ? System
.getProperty("javax.net.ssl.trustStoreProvider") : "";
if (trustStoreType.length() != 0) {
if (trustStoreProvider.length() == 0) {
localKeyStore = KeyStore.getInstance(trustStoreType);
} else {
localKeyStore = KeyStore.getInstance(trustStoreType, trustStoreProvider);
}
char[] keyStorePass = null;
String str5 = System.getProperty("javax.net.ssl.trustStorePassword") != null ? System
.getProperty("javax.net.ssl.trustStorePassword") : "";
if (str5.length() != 0) {
keyStorePass = str5.toCharArray();
}
localKeyStore.load(trustStoreFis, (char[]) keyStorePass);
if (keyStorePass != null) {
for (int i = 0; i < keyStorePass.length; i++) {
keyStorePass[i] = 0;
}
}
}
return (KeyStore) localKeyStore;
} finally {
if (trustStoreFis != null) {
trustStoreFis.close();
}
}
}
private FileInputStream _getFileInputStream(File paramFile) throws Exception {
if (paramFile.exists()) {
return new FileInputStream(paramFile);
}
return null;
}
};
}
});
}
};//Socket
}
}
3.上記クラスを使用したHTTPS通信部分を記載
2で作成したTLSSocketConnectionFactoryクラスとURLConnectionを使って、
HTTPS通信部分を記載します。
※ソースは割と適当なのでsetSSLSocketFactory意外の細かいところは適当に変えてくれよな!
public String sendRequest(String parameter) throws Exception {
log.info(LOG_PREFIX + "通信開始");
// URL設定
URL url = new URL("https://hogehoge.hugahuga.jp/");
HttpsURLConnection connection = null;
try{
/** リクエスト内容設定 **/
connection = (HttpsURLConnection) url.openConnection();
connection.setSSLSocketFactory(new TLSSocketConnectionFactory());
// POST用設定
connection.setRequestMethod("POST");
connection.setDoOutput(true);
// ヘッダ設定
connection.setRequestProperty("Content-Type","application/xml");
// 接続の確立
connection.connect();
// パラメータ設定
PrintWriter writer = new PrintWriter(connection.getOutputStream());
writer.print(parameter);
writer.close();
log.info(LOG_PREFIX + "通信終了");
/** レスポンス読み出し */
StringBuilder sb = new StringBuilder();
// レスポンスコードチェック
if (connection.getResponseCode() == HttpURLConnection.HTTP_OK) {
InputStreamReader isr = new InputStreamReader(connection.getInputStream(), "UTF-8");
BufferedReader reader = new BufferedReader(isr);
String line = null;
// 一行ずつ読み出し
while((line = reader.readLine())!=null) {
sb.append(line);
}
reader.close();
} else {
throw new IllegalHttpStatusException(this.getClass(), "HTTPstatus is not OK");
}
return sb.toString();
} catch(SocketTimeoutException e){
throw new Exception(e);
} catch(IOException e){
throw new Exception(e);
// コネクションのクローズ
} finally {
if(connection != null) {
connection.disconnect();
}
}
}
4.サーバに証明書を追加
接続先の証明書を追加します。
これ忘れるとInternal TLS error, this could be an attackって出たり
HandShakeException出たりして焦ります。
・JAVAに登録されている証明書一覧確認
ex)
keytool /usr/java/使用しているJDKのバージョン/jre/bin/keytool -v -list -keystore
・証明書をJAVAのキーストアに新規登録する
keytool -import -storepass keytoolパスワード(デフォはchangeit) -keystore キーストア(cacerts)の場所 -alias キーストア内の別名 -file 追加する証明書
ex)
keytool -import -storepass changeit -keystore /usr/java/使用しているJDKのバージョン/jre/lib/security/cacerts -alias hogehugakey -file /tmp/hogehuga_root.cer
補足:KeyToolコマンドが認識されないコマンドって出て使えないよ~!って人は、
環境変数にパスが通ってないので、/usr/java/使用しているJDKのバージョン/bin/配下に移動して、
コマンドがあることを確認して使ってくださいねん。rootでな!
ローカルのコマンドプロンプトでパスも正しいのに追加できませんってなったら、管理者で実行してみてください。
ちなみにローカルだとパスがC:\Program Files\java\jdk~ってなって、
ベタ指定するとRuntimeException出たりするので、
パスは”C:\Program Files\java\jdk~”みたいに”で囲ってくれよな!
5.できあがり
以上!いや~~やってみればなんてことはなくめちゃ簡単ですね~
ちなみに3番でApache HTTPClientを使う場合はこんな感じになりますかね。
SSLConnectionSocketFactory socket = new SSLConnectionSocketFactory(
new TLSSocketConnectionFactory(), new String[]{"TLSv1.2"}, null, new DefaultHostnameVerifier());
HttpClient client = HttpClientBuilder.create()
.setSSLSocketFactory(socket)
.build();
あとはもう知りません。私が聞きたいくらいです。(?)
でも、リファレンスをぐぐった時に日本語でまとめられていた時の
安堵感といったら尋常じゃないので、どこかで誰かがいつか救われたらいいな~と思います。
でもやっぱりJavaはアップデートした方がいいと思います()
間違っているところがあったらこっそり教えてください。
ちなみに私の中で味付け海苔No.1はずっと大江のりです。不動のNo.1。
死ぬほど美味しいからぜひ食べてみて欲しい。高いけど。
大野海苔も美味しいです。
日本産の韓国風味付け海苔ってあるのかな~?