Java XML 署名 API の使い方 -署名-

準備

公開鍵暗号方式に関して概念的でもいいからさらっておきましょう。
これがわかってないと何やっているかわからなくなると思います。
(プログラムのロジック自体はそれほど難しくはありません)

ワイは X509 関係の知識があやふやだったので

図解 X.509 証明書』あたりで確認。

特にこの図は素晴らしい。

ただ、後半の自己証明書の作成は、使っているコマンドがモダンすぎて既存の Java コードでは正しく動かなかったりする。(『Mac で OpenSSL』など参照)

ここでも Mac で作業するので、openssl を入れておきます。

brew install openssl

X509 自己署名証明書(オレオレ証明書)を作成する

まずは、秘密鍵を作成。

openssl genrsa 2048 > privatekey.pem

なお、pem は base64 エンコードのテキスト形式です。バイナリ(der 形式)の秘密鍵が欲しい場合は

openssl pkcs8 -in privatekey.pem -outform DER -out privatekey.pk8 -topk8 -nocrypt

などのコマンドで変換しておきます。

次に CSR 証明書を作成します。

openssl req -new -key privatekey.pem -out csr.pem

以下のように質問されますので、適当に答えておきましょう。

You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.

秘密鍵と CSR を使って X509 証明書を作成します。

openssl x509 -req -days 365 -in csr.pem -signkey privatekey.pem -out public.crt

正常に各種証明書が作成されるとフォルダはこんな感じになると思います。

サンプルコード

これで準備が整ったので、コーディング開始。

なんですが、ネット上に参考になるコードがそれなりにあります。

今回は『JavaのXMLデジタル署名APIを利用してXML署名』のコードを使います。

ただし、記事作成時期が古く今となってそのままでは動かないため、以下のように改変。


public class XMLsignB {

    public static void main(String[] args) {
        System.out.println("signed XML");
        
        DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
        Document doc = null;
        try {
            doc = dbf.newDocumentBuilder().parse(new FileInputStream("product.xml"));
        }catch (SAXException ex) {
            Logger.getLogger(XMLsignB.class.getName()).log(Level.SEVERE, null, ex);
        }catch (IOException ex) {
            Logger.getLogger(XMLsignB.class.getName()).log(Level.SEVERE, null, ex);
        } catch (ParserConfigurationException ex) {
            Logger.getLogger(XMLsignB.class.getName()).log(Level.SEVERE, null, ex);
        }
        
        signature(doc);
        
    }
    
    
    private static void signature(Document xmlDom) {

    try {
        // 秘密鍵の取得
        PrivateKey privateKey = getPrivateKey();
        // 証明書の取得
        X509Certificate cert = getCert();

        // 対象要素の親要素にID属性を付与する
        Element targetNode = (Element)xmlDom.getElementsByTagName("ProductInfo").item(0);
        Element parentNode = (Element)targetNode.getParentNode();
        Attr idAttr = xmlDom.createAttribute("id");
        idAttr.setValue("ProductInfo");
        parentNode.setAttributeNode(idAttr);
        parentNode.setIdAttribute("id", true);

        // 署名情報の設定
        XMLSignatureFactory xmlSignFactory = XMLSignatureFactory.getInstance("DOM");

        // 変換アルゴリズムの作成
        ArrayList refTransformList = new ArrayList();
        refTransformList.add(xmlSignFactory.newTransform(Transform.ENVELOPED, (TransformParameterSpec)null));
        refTransformList.add(xmlSignFactory.newTransform(CanonicalizationMethod.EXCLUSIVE, (TransformParameterSpec)null));

        // ダイジェスト計算アルゴリズムの生成
        DigestMethod digestMethod = xmlSignFactory.newDigestMethod(DigestMethod.SHA256, null);

        // 参照要素の設定
        Reference ref = xmlSignFactory.newReference("#ProductInfo", 
                digestMethod, 
                refTransformList, null, null);

        // 正規化アルゴリズムの生成
        CanonicalizationMethod cm = xmlSignFactory.newCanonicalizationMethod(CanonicalizationMethod.EXCLUSIVE, (C14NMethodParameterSpec)null);

        // 署名アルゴリズムの生成
        SignatureMethod sm = xmlSignFactory.newSignatureMethod("http://www.w3.org/2001/04/xmldsig-more#rsa-sha256", null);

        // 署名情報の設定
        SignedInfo signedInfo = xmlSignFactory.newSignedInfo(cm, sm, Collections.singletonList(ref));

        KeyInfoFactory kif = xmlSignFactory.getKeyInfoFactory();
        X509Data x509Data = kif.newX509Data(Collections.singletonList(cert));
        KeyInfo keyInfo = kif.newKeyInfo(Collections.singletonList(x509Data));

        // 署名対象と秘密鍵の設定
        DOMSignContext dsc = new DOMSignContext(privateKey, parentNode);

        // XML署名の実施
        XMLSignature signature = xmlSignFactory.newXMLSignature(signedInfo, keyInfo);
        signature.sign(dsc);

        // コンソールに結果を表示する。
        TransformerFactory tf = TransformerFactory.newInstance();
        Transformer trans = tf.newTransformer();
        OutputStream os = new FileOutputStream("result.xml");
        trans.transform(new DOMSource(xmlDom), new StreamResult(System.out));
        //trans.transform(new DOMSource(xmlDom), new StreamResult(os));//ファイル書き出し

    } catch (Exception e) {
        e.printStackTrace();
    }

}

    /**
     * 秘密鍵の読込
     * 
     * @return
     * @throws NoSuchAlgorithmException
     * @throws IOException
     * @throws InvalidKeySpecException
     */
    private static PrivateKey getPrivateKey() throws NoSuchAlgorithmException, IOException, InvalidKeySpecException {

        byte[] key = readkeyFile("privatekey.pk8");
        PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(key);

        KeyFactory kf = KeyFactory.getInstance("RSA"); 
        RSAPrivateKey privateKey = (RSAPrivateKey)kf.generatePrivate(keySpec);
        return privateKey;
    }

    /**
     * サーバー証明書の読込
     * 
     * @return
     * @throws IOException
     * @throws CertificateException
     */
    private static X509Certificate getCert() throws IOException, CertificateException {

        CertificateFactory cf = CertificateFactory.getInstance("X.509");
        
        X509Certificate cert = (X509Certificate)cf.generateCertificate(new FileInputStream("public.crt"));

        return cert;

    }

    /**
     * バイナリファイルの読込
     * 
     * @param fileName
     * @return
     * @throws IOException
     */
    private static byte[] readkeyFile(String fileName) throws IOException {
        byte[] data = null;
        FileInputStream input = new FileInputStream(fileName);
        data = new byte[input.available()];
        input.read(data);
        input.close();

        return data;
    }

}

Java17 以上で動くと思います。

ファイル書き出しを選んだ場合は、以下のような result.xml が作成されます。

 

参考

実は、公式ドキュメントがわかりやすかったりする。

巷にある解説記事のほとんどは、ここのサンプルコードが素。

時間的に余裕のある人は、XML 署名の公式ページ読みましょう。こことか。
より理解が深まると思います。

 

(適宜情報追加予定)

JCA -Java Cryptography Architecture-

公開鍵暗号あたりを勉強していたとき、巡り合ったのが JCA なるターム。

調べてみたら Java Cryptography Architecture からきているらしい。

どういうものかは、公式の案内やこの記事あたり参照。

 

気になる問題は、マイナカードあたりでは、(秘密鍵による)暗号化自体はカードに格納されているアプリで行うので、どうやって外部暗号化モジュールと連携をはかるかってこと。


・・・とタラタラやっていたら、偉大な某氏が、X で調べ始めた

ええと、実際の JDK のソースにあたるとここら辺の実装が分かってくる。

例えば、XML 署名の際には、sign というメソッドを使うが、そのソースはこうなっている。

XMLsignature クラス自体が abstract なため具体的な実装はなく、やはり暗号化ブロバイダあたりの機能を使っているってことかな。


 

 

 

(適宜情報追加予定)

 

WildFly 運用のための覚え書き

もうあまり使うことはないかもしれないが、Java のアプリケーションサーバ WildFly の運用のための覚え書き。

同一物理マシンに複数の WildFly を設置するのは可能か?

レガシーなプロジェクトで、JavaEE ウェブアプリと JakartaEE ウェブアプリが混在するような場合、少なくとも2台のWildFly が必要になる。27 以降の WildFly と 27 より前のバージョンがそれぞれ少なくとも一つは必要。

結論から言うとできる。

ただ、ポートがかち合わないように設定ファイルを変える。
ワイの場合は、各ポート番号に 10000 を足す。

standalone-full.xml
<socket-binding-group name="standard-sockets" default-interface="public" port-offset="${jboss.socket.binding.port-offset:0}">
        <socket-binding name="ajp" port="${jboss.ajp.port:18009}"/>
        <socket-binding name="http" port="${jboss.http.port:18080}"/>
        <socket-binding name="https" port="${jboss.https.port:18443}"/>
        <socket-binding name="iiop" interface="unsecure" port="13528"/>
        <socket-binding name="iiop-ssl" interface="unsecure" port="13529"/>
        <socket-binding name="management-http" interface="management" port="${jboss.management.http.port:19990}"/>
        <socket-binding name="management-https" interface="management" port="${jboss.management.https.port:19993}"/>
        <socket-binding name="txn-recovery-environment" port="14712"/>
        <socket-binding name="txn-status-manager" port="14713"/>
        <outbound-socket-binding name="mail-smtp">
           <remote-destination host="${jboss.mail.server.host:localhost}" port="${jboss.mail.server.port:10025}"/>
        </outbound-socket-binding>
        <outbound-socket-binding name="messaging-activemq">
            <remote-destination host="${jboss.messaging.connector.host:localhost}" port="${jboss.messaging.connector.port:51616}"/>
        </outbound-socket-binding>
    </socket-binding-group>

ただし、このやり方だと activemq は 71616 となり

A maximum value of 65535 is required

と WildFly に怒られるので、10000 を引く。

こうしておくと以下のように管理画面も同時に表示できる。

めでたしめでたし。

WildFly をサービス化する

JBOSS_HOME の docs 以下のフォルダに各種サンプルファイルがあるので、これをうまく活用する。
Ubuntu の場合は、以下のようにそのまま使える。

sudo mkdir /etc/wildfly
sudo cp /opt/wildfly/docs/contrib/scripts/systemd/wildfly.conf /etc/wildfly
sudo cp /opt/wildfly/docs/contrib/scripts/systemd/wildfly.service /etc/systemd/system/wildfly.service
sudo cp /opt/wildfly/docs/contrib/scripts/systemd/launch.sh /opt/wildfly/bin/launch.sh

この作業が終わった後、初回起動は

sudo systemctl enable --now wildfly

というコマンドで。

jboss-cli.sh による deploy

なお、サービスが走っている場合のデプロイは jboss-cli.sh を走らせ、そこから

deploy PathTo/hoge.war

で投入できる。

バックグランドで起動させる

Java の慣習でしょうか & を付ける。

standalone.sh -c stanalone-full.xml &

Ctrl+C でメッセージを終了させてもプロセスは終わってない模様。

コマンドラインから停止

バックグランドで走らせた状態では Ctrl+C では停止できないので以下のコマンドを使う。

jboss-cli.sh --connect command=shutdown

 

(適宜修正予定)

WildFly の導入時にすること

X などで「わかっているのだが、覚えるには至っておらず、何度も調べてしまうこと」が度々話題になる。

vi の操作方法とかさ。

ワイの場合は WildFly の初期設定もそのうちの一つ。

一体、何度、調べただろう?

いい加減、あちこち調べるのが嫌になってきたので、忘備録的なまとめ。

管理ユーザーの設定

まずは、管理ユーザーを作成するのがいいだろう。
管理ユーザーなしでも WildFly 自体は起動できるのだが、コンソール画面に入れないので、まず、これをやる。

Linux/Mac などでは bin フォルダ内の add-user.sh を実行。Windows では add-user.bat 。

ほとんどの場合、既存プロジェクトの WildFly のバージョンアップデートに伴う移行だと思うので、管理ユーザー名も同一にしておくと迷わなくてすむ。

ただし、この手の作業が面倒に感じる理由の一つとして、「パスワードの設定方法が変わってしまう」というのもあるだろう。
今回、作業した WildFly27 では、以下のように non-alphanumeric symbol が必要とされた。

The password should contain at least 8 characters, 1 alphabetic character(s), 1 digit(s), 1 non-alphanumeric symbol(s)

以前のように「12345678」ではダメである。英数字以外の記号を一つ含める必要がある。

他は、y と答えておいて困ることはない。

モジュールのインストール

デバイスドライバなどをインストールする。

PostgreSQL のドライバをインストールする場合には、以下のようにする。
(ドライバはここから取ってくる)

WildFly 自体を起動した状態で jboss-cli.sh を実行

You are disconnected at the moment. Type 'connect' to connect to the server or 'help' for the list of supported commands.
[disconnected /]

というメッセージが現れるので、connect と打ち込む。

その後

module add --name=org.postgresql --resources=(パス)\postgresql-(バージョン).jar --dependencies=javax.api,jakarta.transaction.api
/subsystem=datasources/jdbc-driver=postgresql:add(driver-name="postgresql",driver-module-name="org.postgresql",driver-class-name="org.postgresql.Driver")

とタイプ。

上のように success が返ってくればOKです。

なお dependencies=javax.api, jakarta.transaction.api となっている点に注意。
いわゆる JavaEE -> JakartaEE 移行に伴う名前空間の変更の影響を受けています。

その他

思いつくままに。

コマンドラインでの終了のさせ方

これも何度検索かけただろう?

jboss-cli.sh --connect --command=":shutdown"

だそうです。

 

(適宜情報追加予定)

ものぐさ RESTEasy -画像データなどを REST っぽく返したい-

『ものぐさ Jersey』的なやつが好評のようなので、その RESTEasy 編。

いきなりで恐縮だが、REST なウェブサービスを作っている際に画像も REST 的に取り扱いたいときがある。
要するに適当なエンドポイントを叩くと画像ファイルが返ってくる、みたいなやつだ。

サーブレットで実現するのが一番簡単なようなんだが、サーブレットを使わない方向でチャレンジする。

リンクした記事は jsp で書いてあるが、ちなみにサーブレットで書き直すとこんな感じ↓になる。

あっさり成功。

 

で、RESTEasy 。
まずは公式ドキュメントのチェック。

レスポンスを適当に加工すれば、なんとかなるだろうくらいに思って調べてみた。

なんだが…

なんでしょう、この突き放された感じ(笑)。

jakarta.ws.rs.core.StreamingOutput を使って自分で実装しろ的なことが書かれている。

いや、そんなことをするくらいならば、サーブレットのやつそのまま使いたいかなあ(困惑)。

アウトプットストリーム系を使って、とりあえずバイト列を返す

他の人がどうやっているかよくわからないのだが、まずはシンプルに攻める。

(公式ドキュメントのサジェスチョンを尊重して)バイト列を生成して、そいつを ByteArrayOutputStream にセットし、さらにそれを Response の中(Entity とかいうみたいだ)に入れ込んで REST っぽく返す、と以下のような感じになる。


@Path("/octet")
public class OctetServer {

 @GET
 @Produces(MediaType.APPLICATION_OCTET_STREAM)
 public Response sendOctet() throws IOException{

  byte[] byteData = "123456789ABCDE".getBytes();

  ByteArrayOutputStream output = new ByteArrayOutputStream();
  output.write(byteData);

  return Response.ok(output.toByteArray(), MediaType.APPLICATION_OCTET_STREAM).build();

 }

}

これで curl で (コンテクスト)/octet を叩くと、もちろん 123456789ABCDE がバイナリで返ってくる。
ブラウザからアクセスすると octet というファイルを落としてくれるが、中身は当然 123456789ABCDE だ。

これで、第一段階はクリア…

と思っていたが、コード見直すと、バイト列をストリームにセットした後、さらにバイト列に戻す、というバカっぽいことをやっている(苦笑)。

バイト列をエンティティとしても同じ

だから、上のサンプルはもうちょっと簡単になる。

具体的には、

return Response.ok(output.toByteArray(), MediaType.APPLICATION_OCTET_STREAM).build();

は、

return Response.ok(byteData, MediaType.APPLICATION_OCTET_STREAM).build();

としても変わらない。

もちろん、その上の2行は全く不要。

とりあえず、バイナリのデータを返すだけであれば、アウトプットストリーム系は不要で返したいデータのバイト列を response のエンティティにセットすればいいだけのようだ。

指定した画像などを返したければ、path parameter を設定してデータベースから検索して云々とやれば、実現できそう。