23
30

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 5 years have passed since last update.

電子証明書あれこれ

Last updated at Posted at 2017-09-22

#やりたいこと
XML署名について初めての実装なので、いろいろメモを残しつつ進めることに。

  • JavaでXML署名をしたい
  • 電子証明書の形式はPKCS12形式

#運用

  1. PKCS12形式(.p12)の証明書をJavaのキーストア(cacert)にインポートする
  2. (必要であれば)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コード

createSignedXml.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;
    }
23
30
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
23
30

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?