電子証明書あれこれ

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

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

#運用

1. PKCS12形式（.p12）の証明書をJavaのキーストア（cacert）にインポートする
2. （必要であれば）DBから取得している各パラメータを修正<br>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コード

```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;
    }
```