#やりたいこと
XML署名について初めての実装なので、いろいろメモを残しつつ進めることに。
- JavaでXML署名をしたい
- 電子証明書の形式はPKCS12形式
#運用
- PKCS12形式(.p12)の証明書をJavaのキーストア(cacert)にインポートする
- (必要であれば)DBから取得している各パラメータを修正
cacertのパス、キーストアのパスワード、エイリアス、暗号化アルゴリズム、ダイジェストアルゴリズム、署名アルゴリズムなど。。。
電子証明書の入れ替えのたびに上記運用が必要となる。
#調べもの
##電子証明書とは??
電子証明書の「正規化」とは?
http://www.atmarkit.co.jp/fxml/tanpatsu/16xmlsecurity/xmlsecurity02.html
RSA鍵、証明書のファイルフォーマットについて
https://qiita.com/kunichiko/items/12cbccaadcbf41c72735
PKCS #12 ファイルから秘密鍵,証明書,中間CAを取り出す
https://qiita.com/cs_sonar/items/f2753ffdebb1c67d5302
自己署名証明書の作成方法
https://qiita.com/akito1986/items/8eb41f5a43bb9421ae79
##JavaでXML署名するには??
JavaのXMLデジタル署名APIを利用してXML署名
http://qiita.com/KevinFQ/items/4e2484a659b618530e72
keytoolコマンド一覧
http://itref.fc2web.com/java/keytool.html
【Java】 Javaで、キーストアから証明書を追加・削除などを行う
https://blogs.yahoo.co.jp/dk521123/37097725.html
#いろいろメモ
サーバー証明書 pem ⇒ crtに変換
$ openssl x509 -outform der in server-crt.pem -out server-crt.crt
サーバー証明書 pem ⇒ p12に変換
$ openssl pkcs12 -export -out server-crt.p12 -in server-crt.pem -inkey server-privatekey.pem -passin pass:root -passout pass:root
サーバー証明書 p12 ⇒ crtに変換
$ openssl pkcs12 -in server-crt.p12 -clcerts -nokeys -out server-crt.crt
サーバー証明書 p12 ⇒ cerに変換
$ openssl pkcs12 -in server-crt.p12 -des -out server-crt.cer
##keytool(鍵と証明書の管理ツール)あれこれ
###keytoolのインストール
https://jp.globalsign.com/support/faq/331.html
###キーストアファイルを作成
コマンド参照:https://docs.oracle.com/javase/jp/6/technotes/tools/windows/keytool.html
cacertsに変更が生じるとJavaが自動アップデートができなくなってしまうらしい。バックアップ必須。http://d.hatena.ne.jp/uunfo/20110813/1313242092
keytool -genkeypair -keysize 2048 -keyalg RSA -sigalg SHA256withRSA -alias testcrt -keystore "C:\Program Files\Java\jreX.X.X_XXX\lib\security\cacerts" -storepass changeit
2017/10/16追加
任意の場所にキーストアファイルを作成
(共通名がtest1、組織単位がtest2、組織がtest3、2文字の国コードがJPの場合)
$ keytool -genkey -dname "cn=test1, ou=test2, o=test3, c=JP" -keystore (キーストアファイル名.keystore) -alias (証明書・鍵をKeyStore内で識別する名前)-keypass (証明書・鍵のパスワード ※6文字以上) -storepass (キーストアのパスワード) -keyalg RSA -keysize 2048 -validity 10000
参考(鍵ペアの生成あたり)
https://docs.oracle.com/javase/jp/8/docs/technotes/tools/unix/keytool.html
###サーバー証明書をキーストアに追加する
外部リソースとして証明書をむき出しで置いておくのはどうかと思ったため、キーストアに証明書をインポートすることを検討。
※以下、カレントディレクトリに証明書がある場合
cerファイルをキーストアにインポート
※キーストアの初期パスワード:changeit
crtファイルを以下コマンドでインポートしようとすると「keytoolエラー: java.lang.Exception: 入力はX.509証明書ではありません」とエラーが出てしまう。cerでインポートする。
$ keytool -import -alias testkey -keystore "C:\Program Files\Java\jreX.X.X_XXX\lib\security\cacerts" -file server-crt.cer
p12ファイルをキーストアにインポート
http://blogger.fastriver.net/2014/10/keytool.html
エイリアス(識別名)を指定しないとデフォルトで「1」という名前でインポートされます。
エイリアスをつけたい場合は、「-srcalias 1 -destalias (任意のエイリアス名)」のようにオプションをつけます。
$ keytool -importkeystore -keystore "C:\Program Files\Java\jreX.X.X_XXX\lib\security\cacerts" -srckeystore server-crt.p12 -srcstoretype PKCS12 -srcalias 1 -destalias testcrt -srcstorepass root -deststorepass changeit
インポートした証明書を確認
$ keytool -list -v -alias testcrt -keystore "C:\Program Files\Java\jreX.X.X_XXX\lib\security\cacerts" -keypass root -storepass changeit
###キーストアからインポート済みの証明書を削除する
$ keytool -delete -alias testcrt -keystore "C:\Program Files\Java\jreX.X.X_XXX\lib\security\cacerts" -keypass root -storepass changeit
###エイリアス名を変更する(11111111⇒22222222に変更する場合)
$ keytool -changealias -srcalias 11111111 -destalias 22222222 -keystore "C:\Program Files\Java\jreX.X.X_XXX\lib\security\cacerts" -keypass root -storepass changeit
###証明書の有効期限を延長する(2000日後に設定する場合)
$ keytool -selfcert -alias testcrt -validity 2000 -keystore "C:\Program Files\Java\jreX.X.X_XXX\lib\security\cacerts" -keypass root -storepass changeit
##完成したJavaコード
public String createSignedXml(String fileName)
throws Exception {
// Documentインスタンスの生成
DocumentBuilder documentBuilder = null;
documentBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
Document document = documentBuilder.newDocument();
// XML文書のDOM生成(省略)
// XML署名作成
// p12ファイルパス
File certDir = "XXXXXX";
// certificateディレクトリ配下の証明書ファイルリスト(1ファイルのみ存在することを想定)
File[] certP12List = certDir.listFiles((FileFilter) new SuffixFileFilter(".p12"));
// 証明書ファイル
File certP12File = null;
// キーストアファイルのパスを取得
String keyStoreFilePath = "/usr/java/jdkX.X.X_XXX/jre/lib/security/testkeystore";
// キーストアファイルを取得
File keyStoreFile = new File(keyStoreFilePath);
// キーストアのパスワード
String keyStorePass = "root";
// 証明書のパスワード
String certificatePass = "changeit";
// p12ファイルが複数配置されていた場合はエラー
if (certP12List.length == 1) {
certP12File = certP12List[0];
} else {
throw new IOException();
}
try {
// キーストアを取得する
KeyStore keyStore = getKeyStore(keyStoreFile, keyStorePass);
// 識別名:XXXに合致する証明書をキーストアから取得する
X509Certificate x509cert = getCertificate(keyStore, XXX);
// 証明書インポート判定用フラグ
boolean importFlg = false;
// 有効期限チェック用
// 0: インポート済み証明書が有効期限内, 1: インポート済み証明書が有効期限切れ,
// 2: 外部リソースの証明書が有効期限切れ, 3: ともに有効期限切れ, 4: インポートエラー
int checkResult = 0;
if (x509cert != null) {
// 外部リソースに配置されている証明書とインポート済みの証明書の有効期限チェック
checkResult = checkCertificateExpired(x509cert, XXX, keyStoreFile, certP12File, keyStorePass, certificatePass);
// インポート済み証明書が有効期限切れ
if (checkResult == 1) {
// 識別名:XXXの証明書の削除を実行
deleteProcessExec(XXX, keyStoreFile, certificatePass, keyStorePass);
importFlg = true;
}
} else {
importFlg = true;
}
// キーストアにインポートされている証明書の有効期限が切れていた場合は
// 新しい証明書をインポートし直す
if (importFlg) {
// 証明書のインポートを実行
importProcessExec(keyStoreFile, XXX, certP12File, certificatePass, keyStorePass);
// キーストアを再取得する
keyStore = getKeyStore(keyStoreFile, keyStorePass);
// 識別名:XXXに合致する証明書をキーストアから再取得する
x509cert = getCertificate(keyStore, XXX);
// 有効期限チェック
checkResult = checkCertificateExpired(x509cert, keyStoreFile, certP12File, keyStorePass, certificatePass);
}
// 外部リソースに配置されている証明書とインポート済みの証明書がともに有効期限切れ
// 外部リソースの証明書インポートエラー
if (checkResult >= 3) {
// 識別名:XXXの証明書の削除を実行
deleteProcessExec(XXX, keyStoreFile, certificatePass, keyStorePass);
throw new CertificateException();
}
// 秘密鍵を取得
RSAPrivateKey privateKey = (RSAPrivateKey) keyStore.getKey(XXX, certificatePassArray);
// 署名コンテキストの作成
DOMSignContext dsc = new DOMSignContext(privateKey, document.getDocumentElement());
XMLSignatureFactory fac = XMLSignatureFactory.getInstance("DOM");
// 参照要素の作成
Reference ref = fac.newReference
("#XXXXXX", fac.newDigestMethod(DigestMethod.SHA256, null),
Collections.singletonList
(fac.newTransform(Transform.ENVELOPED,
(TransformParameterSpec) null)), null, null);
// 署名情報の作成
SignedInfo si = fac.newSignedInfo
(fac.newCanonicalizationMethod
(CanonicalizationMethod.INCLUSIVE_WITH_COMMENTS,
(C14NMethodParameterSpec) null),
fac.newSignatureMethod("http://www.w3.org/2001/04/xmldsig-more#rsa-sha256", null),
Collections.singletonList(ref));
KeyInfoFactory kif = fac.getKeyInfoFactory();
X509Data x509Data = kif.newX509Data(Collections.singletonList(x509cert));
KeyInfo ki = kif.newKeyInfo(Collections.singletonList(x509Data));
// 署名する.
XMLSignature signature = fac.newXMLSignature(si, ki);
signature.sign(dsc);
} catch (XMLSignatureException e) {
throw new XMLSignatureException("XML署名の生成中または検証処理中にエラーが発生", e);
} catch (MarshalException e) {
throw new MarshalException(e);
} catch (InvalidAlgorithmParameterException e) {
throw new InvalidAlgorithmParameterException(e);
} catch (NoSuchAlgorithmException e) {
throw new NoSuchAlgorithmException(e);
} catch (KeyStoreException e) {
throw new KeyStoreException(e);
} catch (CertificateException e) {
throw new CertificateException(e);
} catch (UnrecoverableKeyException e) {
throw new UnrecoverableKeyException();
}
return this.write(document);
}
/**
* キーストアを取得する
*
* @param keyStoreFile キーストアファイル
* @param keyStorePass キーストアのパスワード
* @return keyStore キーストアオブジェクト
*/
private KeyStore getKeyStore(File keyStoreFile, String keyStorePass) {
KeyStore keyStore = null;
try (FileInputStream keyStoreStream = new FileInputStream(keyStoreFile)) {
// キーストアのタイプ:JKS(PKCS12の場合は引数を"PKCS12"に修正する)
keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
keyStore.load(keyStoreStream, keyStorePass.toCharArray());
} catch (KeyStoreException | NoSuchAlgorithmException | CertificateException | IOException e) {
// TODO 自動生成された catch ブロック
e.printStackTrace();
}
return keyStore;
}
/**
* 識別名に合致する証明書をキーストアから取得する
*
* @param keyStore キーストアオブジェクト
* @param alias 識別名
* @return certificate 証明書オブジェクト
*/
private X509Certificate getCertificate(KeyStore keyStore, String alias){
X509Certificate certificate = null;
try {
certificate = (X509Certificate) keyStore.getCertificate(alias);
} catch (KeyStoreException e) {
// TODO 自動生成された catch ブロック
e.printStackTrace();
}
return certificate;
}
/**
* 証明書の有効期限切れチェック
*
* @param x509cert X509証明書オブジェクト
* @param keyStoreFile キーストアファイル
* @param certFile 証明書ファイル
* @param keyStorePass キーストアパスワード
* @param certificatePass 証明書パスワード
* @param srcAlias インポート直後の一時的な識別名
* @return 0: インポート済み証明書が有効期限内, 1: インポート済み証明書が有効期限切れ,
* 2: 外部リソースの証明書が有効期限切れ, 3: ともに有効期限切れ, 4:外部リソースの証明書インポートエラー
*/
private int checkCertificateExpired(X509Certificate x509cert,
File keyStoreFile, File certFile, String keyStorePass, String certificatePass,
String srcAlias) {
// 処理結果
int result = 0;
// インポート済み証明書の有効期限切れフラグ
// true: 有効期限切れ, false: 有効期限内
boolean importExpiredFlg = false;
// 外部リソースに配置された証明書の有効期限切れフラグ
boolean extExpiredFlg = false;
// キーストアにインポートされている証明書の有効期限を取得
Date importExpiredDate = x509cert.getNotAfter();
// 現在日時
Date now = new Date();
/************************** インポート済み証明書の有効期限チェック START **************************/
// インポート済み証明書の有効期限が切れていないか判定
if (now.compareTo(importExpiredDate) > 0) {
// 有効期限切れの場合
importExpiredFlg = true;
result = 1;
} else {
// 有効期限内のため、以降のチェック不要
return 0;
}
/*************************** インポート済み証明書の有効期限チェック END ***************************/
/*********************** 外部リソースに配置された証明書の有効期限チェック START **********************/
try {
// 一時的な識別名
String destAlias = System.currentTimeMillis();
// インポートコマンド実行
importProcessExec(keyStoreFile, destAlias, certFile, certificatePass, keyStorePass);
// キーストアを取得する
KeyStore tmpKeyStore = getKeyStore(keyStoreFile, keyStorePass);
// 識別名:destAliasに合致する証明書をキーストアから取得する
X509Certificate tmpCert = getCertificate(tmpKeyStore, destAlias);
// インポートした証明書の有効期限を取得
Date extExpiredDate = null;
if (tmpCert != null) {
extExpiredDate = tmpCert.getNotAfter();
} else {
// インポートエラー
return 4;
}
// 現在時刻と外部リソースの証明書の有効期限を比較
if (now.compareTo(extExpiredDate) > 0) {
// 有効期限切れの場合
extExpiredFlg = true;
result = 2;
}
// 一時的にインポートした証明書の削除コマンド実行
deleteProcessExec(destAlias, keyStoreFile, certificatePass, keyStorePass);
} catch(InterruptedException e) {
// TODO 自動生成された catch ブロック
e.printStackTrace();
}
/********************** 外部リソースに配置された証明書の有効期限チェック END *********************/
// インポート済み証明書と外部リソースに配置された証明書
// いずれも有効期限切れの場合はresult:3
if (importExpiredFlg && extExpiredFlg) {
result = 3;
}
return result;
}
/**
* 証明書削除コマンドを実行する.
*
* @param alias 識別名
* @param keyStoreFile キーストアファイル
* @param certificatePass 証明書パスワード
* @param keyStorePass キーストアパスワード
* @return true: 正常終了, false: 異常終了
* @throws InterruptedException
*/
private boolean deleteProcessExec(String alias, File keyStoreFile, String certificatePass,
String keyStorePass)
throws InterruptedException {
// キーストアにインポートされている証明書を削除するコマンドを取得する
List<String> command = getDeleteCommand(alias, keyStoreFile, certificatePass, keyStorePass);
// 削除コマンド実行
return processExec(command);
}
/**
* 証明書削除コマンドを取得する.
*
* @param alias 識別名
* @param keyStoreFile キーストアファイル
* @param certificatePass 証明書パスワード
* @param keyStorePass キーストアパスワード
* @return resultList 削除コマンド
*/
private List<String> getDeleteCommand(String alias, File keyStoreFile, String certificatePass,
String keyStorePass) {
// キーストアにインポートされている証明書を削除するコマンド
String command = "keytool -delete -alias %alias% -keystore %keyStoreFilePath% -keypass %certificatePass% -storepass %keyStorePass%";
// コマンドを文字列配列に分割
String[] commandList = command.split(" ");
List<String> resultList = new ArrayList<String>();
for (String cmd : commandList) {
switch (cmd) {
case "%alias%":
cmd = cmd.replace("%alias%", alias);
break;
case "%keyStoreFilePath%":
cmd = cmd.replace("%keyStoreFilePath%", keyStoreFile.getPath());
break;
case "%certificatePass%":
cmd = cmd.replace("%certificatePass%", certificatePass);
break;
case "%keyStorePass%":
cmd = cmd.replace("%keyStorePass%", keyStorePass);
break;
}
resultList.add(cmd);
}
return resultList;
}
/**
* 証明書インポートコマンドを実行する.
*
* @param certFilePath 証明書ファイル
* @param destAlias 識別名
* @param keyStoreFilePath キーストアファイル
* @param certificatePass 証明書パスワード
* @param keyStorePass キーストアパスワード
* @return true: 正常終了, false: 異常終了
* @throws InterruptedException
*/
private boolean importProcessExec(File keyStoreFile, String destAlias, File certFile,
String certificatePass,
String keyStorePass) throws InterruptedException {
// 証明書のインポートを行うコマンド
List<String> command = getImportCommand(keyStoreFile, destAlias, certFile, certificatePass,
keyStorePass);
// インポートコマンド実行
return processExec(command);
}
/**
* 証明書インポートコマンドを取得する.
*
* @param certFilePath 証明書ファイル
* @param destAlias 識別名
* @param keyStoreFilePath キーストアファイル
* @param certificatePass 証明書パスワード
* @param keyStorePass キーストアパスワード
* @return resultList インポートコマンド
*/
private List<String> getImportCommand(File keyStoreFile, String destAlias, File certFile,
String certificatePass, String keyStorePass) {
// 証明書のインポートを行うコマンド
String command = "keytool -importkeystore -keystore %keyStoreFilePath% -srckeystore %certFilePath% -srcstoretype PKCS12 -srcalias 1 -destalias %destalias% -srcstorepass %certificatePass% -deststorepass %keyStorePass%";
// コマンドを文字列配列に分割
String[] commandList = command.split(" ");
List<String> resultList = new ArrayList<String>();
for (String cmd : commandList) {
switch (cmd) {
case "%keyStoreFilePath%":
cmd = cmd.replace("%keyStoreFilePath%", keyStoreFile.getPath());
break;
case "%certFilePath%":
cmd = cmd.replace("%certFilePath%", certFile.getPath());
break;
case "%destalias%":
cmd = cmd.replace("%destalias%", destAlias);
break;
case "%certificatePass%":
cmd = cmd.replace("%certificatePass%", certificatePass);
break;
case "%keyStorePass%":
cmd = cmd.replace("%keyStorePass%", keyStorePass);
break;
}
resultList.add(cmd);
}
return resultList;
}
/**
* 外部プロセスを実行する.
*
* @param command コマンド内容
* @return true: 正常終了, false: 異常終了
*/
private boolean processExec(List<String> command) {
// 処理結果
boolean result = false;
try {
ProcessBuilder processBuilder = new ProcessBuilder(command);
Process Process = processBuilder.start();
// プロセスの正常終了まで待機させる
if (Process.waitFor() == 0) {
result = true;
log.info("Process Success: " + command.toString());
} else {
log.warn("Process Failed: " + command.toString());
}
// 標準出力
String strInput;
BufferedReader ipbr = new BufferedReader(new InputStreamReader(Process.getInputStream()));
while((strInput = ipbr.readLine()) != null) {
log.info(strInput);
}
ipbr.close();
// エラー出力
String strErr;
BufferedReader erbr = new BufferedReader(new InputStreamReader(Process.getErrorStream()));
while((strErr = erbr.readLine()) != null) {
log.info(strErr);
}
erbr.close();
//ProcessBuilderを使用後、バックグラウンドでInputStream, OutputStream, ErrorStreamがオープンされる.
// リソース不足を回避するため全てのストリームをクローズする.
Process.getInputStream().close();
Process.getOutputStream().close();
Process.getErrorStream().close();
} catch (InterruptedException | IOException e) {
// TODO 自動生成された catch ブロック
e.printStackTrace();
}
return result;
}