Spring Bootでspring-boot-starter-parentじゃない親pomを使いつつ、Fully Executable Warにする。

最近はSpringとかSpring Bootで組むあんなのとかそんなのを開発しております。

<plugin>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-maven-plugin</artifactId>
    <configuration>
        <executable>true</executable>
    </configuration>
</plugin>

こんな感じで、Fully Executable War というのになるように作ると、(WebアプリならTomcatまで含めて)依存関係全部込みのJarファイルを作ってくれた上に、Jarファイルの頭にシェルスクリプトをつけて、/etc/init.d/ からシンボリックリンクを張れば、そのままサービスにできちゃうファイルを作ってくれる便利さなのですが、 なぜかこれがうまくいかない。

原因は、pom.xmlで、親を

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>1.4.1.RELEASE</version>
</parent>

とするのではなく、いろんな事情で親にしないといけない別の親pomを指定しながら、

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-dependencies</artifactId>
            <version>${spring.boot.version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

これでspring-boot-dependenciesを指定してたら、だいたい同じことをしてくれるのかなと思ってたらmavenプラグインの設定に関しては何もしてくれなかったため。

<plugin>
        <groupId>org.springframework.boot</groupId>
         <artifactId>spring-boot-maven-plugin</artifactId>
         <version>${spring.boot.version}</version>
         <executions>
            <execution>
                 <goals>
                     <goal>repackage</goal>
                 </goals>
            </execution>
        </executions>
        <configuration>
            <executable>true</executable>
        </configuration>
</plugin>

こんな感じで、repackageの設定をする必要がありました。

Javaでテスト書くときのstatic import

仕事やめたり復職したりしてました。

ところでJavaユニットテスト書くときにstatic importを多用するのですが、 IntelliJ使うことにしたのですが、static importEclipseと違ってうまく補完してくれませんしorganize importとかしてると、使ってない隙を突いて消してしまうので、LiveTemplateに以下を保存。

import static org.junit.Assert.*;
import static org.hamcrest.Matchers.*;
import static org.mockito.Mockito.*;

import static org.junit.Assert.*;

assert***とかfailがここに入ってる。 assertThatも最近のJUnitならこっちに入ってるので、hamcrestのMatcherAssertはインポート不要。

import static org.hamcrest.Matchers.*;

JUnitに含まれているorg.hamcrest.CoreMatchers と違って、hamcrestの全部入ってる版を入れないといけないが、 入ってるならMatchersはCoreMathcersのスーパーセットなので、こっちでよい。

import static org.mockito.Mockito.*;

Mockitoのverifyとかwhenとかはここ。

import static org.mockito.ArgumentMatchers.*;

Mockitoのanyとかがここ。org.mockito.Matchers はdeprecatedとなり、これを使うのが正しいようだ。

ArgumentMatchersはMockito2から。そしてMockitoがArgumentMatchersを継承しているので、これは要らない。

cocoapodsがインストールできない

  • 家のMacにgem install cocoapods すると、SIZEOF_LONGがどうこうと言われる。rubyがEl Capitanので、2.0とやや古いのが行けなさそうな雰囲気。
  • rbenvでruby 2.3を入れようとするとssl周りで怒られて駄目なので、まずport install opensslでopensslを入れる。2.3のインストールに成功。
  • で、gem install cocoapods でpodがインストールできたが、事情があってcocoapods 0.39.0を使っていた(1.0系のためのPodfileの変更がまだできてない)ので、pod installするとto_aryが無いと怒られる。
  • どうもruby 2.3に対応してるのはcocoapods 1.0以降であるらしい。ということで、ruby2.2 を入れて、gem install cocoapods もやり直して(やり直さないとruby 2.3を使うpodコマンドが残る。)ようやくpod installに成功。

unrecognized command line option "-std=gnu++0x"

electronのバージョンを上げるとnodejsのバージョンを上げないと使ってるモジュールが構文エラーになるようで仕方ないのでバージョンを上げたりnpmモジュールのバージョンを上げたりなどしているとこんな感じのエラーでfs-xattrというnpmモジュールのコンパイルが通らなくて困った。

cc1plus: error: unrecognized command line option "-std=gnu++0x"

g++に、apple-gcc42が使われていたせいなので、

$ sudo port install gcc5
$ port select --list gcc              [2016-05-09 19:44 kozawa-lisb]
Available versions for gcc:
    apple-gcc42 (active)
    mp-gcc48
    mp-gcc5
    mp-llvm-gcc42
    none
$ sudo port select --set gcc mp-gcc5  [2016-05-09 19:44 kozawa-lisb]
Selecting 'mp-gcc5' for 'gcc' succeeded. 'mp-gcc5' is now active.
$ c++ --version                       [2016-05-09 19:44 kozawa-lisb]
c++ (MacPorts gcc5 5.3.0_1) 5.3.0
Copyright (C) 2015 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

こんな感じでgcc5を入れて解決できた。

elasticsearchのプラグイン更新

どうもelasticsearchのプラグインは、elasticsearch本体バージョンに厳しいようで、たとえば本体2.2と一緒に入れたプラグインが本体2.3ではサポートされていなくて、入れ直す必要があったりとかする様子。

yumでelasticsearchを入れて、sudo bin/plugin install analysis-icuとかでプラグインを入れたあと、yum updateとかAnsibleのyum: name=* state=latestをかけてしまい、elasticsearch本体が更新されてしまったりとかすると、起動時に転ける。

そんなときは一回消して入れ直す必要がある。

sudo bin/plugin remove analysis-icu
sudo bin/plugin install analysis-icu

電子署名、特にXMLの。(後編)

JavaXMLファイルに署名したり検証したりするプログラムの書き方。

必要なものは概ねJavaSEのjavax.xml.cryptoや`javax.xml.crypto.dsig``などのパッケージに全て揃っているので、 そのチュートリアルであるJava XML デジタル署名 API を見ながら写経していく。

公開鍵、秘密鍵の読み込み

こんな感じのコードで、前編で用意したder形式鍵ファイルを読み込むことが出来る。

import java.net.URI;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.KeyFactory;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;

public class KeyUtils {
    public static PrivateKey parsePrivateKey(URI uri) throws Exception {
        byte[] keyBytes = Files.readAllBytes(Paths.get(uri));
        PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(keyBytes);
        KeyFactory kf = KeyFactory.getInstance("RSA");
        return kf.generatePrivate(spec);
    }

    public static PublicKey parsePublicKey(URI uri) throws Exception {
        byte[] keyBytes = Files.readAllBytes(Paths.get(uri));
        X509EncodedKeySpec spec = new X509EncodedKeySpec(keyBytes);
        KeyFactory kf = KeyFactory.getInstance("RSA");
        return kf.generatePublic(spec);
    }
}

署名

リンクになっていないのでGenEnveloped.javaはここにある。

整えながら写経していくと大体こんな感じになる。入出力はInputStream/OutputStreamにした。
これで、「署名を署名対象XMLの最後の子要素として埋め込んだ署名済みXML」が手に入る。
XML電子署名の仕方には署名だけを作るものなど他にも色々あるが、ひとまず今回はこれが出来れば用が足りた。)

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.util.Collections;

import javax.xml.crypto.dsig.CanonicalizationMethod;
import javax.xml.crypto.dsig.DigestMethod;
import javax.xml.crypto.dsig.Reference;
import javax.xml.crypto.dsig.SignatureMethod;
import javax.xml.crypto.dsig.SignedInfo;
import javax.xml.crypto.dsig.Transform;
import javax.xml.crypto.dsig.XMLSignature;
import javax.xml.crypto.dsig.XMLSignatureFactory;
import javax.xml.crypto.dsig.dom.DOMSignContext;
import javax.xml.crypto.dsig.keyinfo.KeyInfo;
import javax.xml.crypto.dsig.keyinfo.KeyInfoFactory;
import javax.xml.crypto.dsig.keyinfo.KeyValue;
import javax.xml.crypto.dsig.spec.C14NMethodParameterSpec;
import javax.xml.crypto.dsig.spec.TransformParameterSpec;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;

import org.w3c.dom.Document;

public class Signer {

    public void sign(InputStream in, OutputStream out, PrivateKey privateKey, PublicKey publicKey)
            throws Exception {
        DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
        dbf.setNamespaceAware(true);
        DocumentBuilder builder = dbf.newDocumentBuilder();
        Document doc = builder.parse(in);
        DOMSignContext dsc = new DOMSignContext(privateKey, doc.getDocumentElement());
        XMLSignatureFactory fac = XMLSignatureFactory.getInstance("DOM");

        Reference ref =
            fac.newReference(
                "",
                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(
                SignatureMethod.RSA_SHA1,
                null), Collections.singletonList(ref));

        KeyInfoFactory kif = fac.getKeyInfoFactory();

        KeyValue kv = kif.newKeyValue(publicKey);
        KeyInfo ki = kif.newKeyInfo(Collections.singletonList(kv));

        XMLSignature signature = fac.newXMLSignature(si, ki);
        signature.sign(dsc);

        TransformerFactory tf = TransformerFactory.newInstance();
        Transformer trans = tf.newTransformer();
        trans.transform(new DOMSource(doc), new StreamResult(out));
    }

}

検証

こちらもおなじく、サンプルプログラムValidate.javaはここにある。SimpleKeySelectorResultというクラスが必要になるが、これはチュートリアルには書かれておらず、サンプルプログラムにのみある。

KeyValueKeySelectorは署名済みXMLファイルから公開鍵情報を取得するコードだが、DOMValidateContextは公開鍵PublicKeyインスタンスを引数にとることができて、この場合、この公開鍵で検証をする。今回使いたかった用途にはこちらの方が実は合っているので、Optionalで引数にPublicKeyインスタンスをもらっていたらそれを使うようにした。

import java.io.FileInputStream;
import java.io.InputStream;
import java.security.Key;
import java.security.KeyException;
import java.security.PublicKey;
import java.util.List;
import java.util.Optional;

import javax.xml.crypto.AlgorithmMethod;
import javax.xml.crypto.KeySelector;
import javax.xml.crypto.KeySelectorException;
import javax.xml.crypto.KeySelectorResult;
import javax.xml.crypto.XMLCryptoContext;
import javax.xml.crypto.XMLStructure;
import javax.xml.crypto.dsig.SignatureMethod;
import javax.xml.crypto.dsig.XMLSignature;
import javax.xml.crypto.dsig.XMLSignatureException;
import javax.xml.crypto.dsig.XMLSignatureFactory;
import javax.xml.crypto.dsig.dom.DOMValidateContext;
import javax.xml.crypto.dsig.keyinfo.KeyInfo;
import javax.xml.crypto.dsig.keyinfo.KeyValue;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;

import org.w3c.dom.Document;
import org.w3c.dom.NodeList;

public class Verifier {

    private static class KeyValueKeySelector extends KeySelector {

        public KeySelectorResult select(KeyInfo keyInfo, KeySelector.Purpose purpose,
                AlgorithmMethod method, XMLCryptoContext context) throws KeySelectorException {

            if (keyInfo == null) {
                throw new KeySelectorException("Null KeyInfo object!");
            }
            SignatureMethod sm = (SignatureMethod) method;
            @SuppressWarnings("unchecked")
            List<XMLStructure> list = keyInfo.getContent();

            for (int i = 0; i < list.size(); i++) {
                XMLStructure xmlStructure = list.get(i);
                if (xmlStructure instanceof KeyValue) {
                    PublicKey pk = null;
                    try {
                        pk = ((KeyValue) xmlStructure).getPublicKey();
                    } catch (KeyException ke) {
                        throw new KeySelectorException(ke);
                    }
                    // make sure algorithm is compatible with method
                    if (algEquals(sm.getAlgorithm(), pk.getAlgorithm())) {
                        return new SimpleKeySelectorResult(pk);
                    }
                }
            }
            throw new KeySelectorException("No KeyValue element found!");
        }

        static boolean algEquals(String algURI, String algName) {
            if (algName.equalsIgnoreCase("DSA")
                && algURI.equalsIgnoreCase(SignatureMethod.DSA_SHA1)) {
                return true;
            } else if (algName.equalsIgnoreCase("RSA")
                && algURI.equalsIgnoreCase(SignatureMethod.RSA_SHA1)) {
                return true;
            } else {
                return false;
            }
        }
    }

    private static class SimpleKeySelectorResult implements KeySelectorResult {
        private PublicKey pk;
        SimpleKeySelectorResult(PublicKey pk) {
            this.pk = pk;
        }

        public Key getKey() { return pk; }
    }


    public boolean verify(InputStream in, Optional<PublicKey> publicKey) throws Exception {
        DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
        dbf.setNamespaceAware(true);
        DocumentBuilder builder = dbf.newDocumentBuilder();
        Document doc = builder.parse(in);
        NodeList nl = doc.getElementsByTagNameNS(XMLSignature.XMLNS, "Signature");
        if (nl.getLength() == 0) {
            throw new Exception("Cannot find Signature element");
        }

        DOMValidateContext valContext =
            publicKey.isPresent()
                ? new DOMValidateContext(publicKey.get(), nl.item(0))
                : new DOMValidateContext(new KeyValueKeySelector(), nl.item(0));

        XMLSignatureFactory factory = XMLSignatureFactory.getInstance("DOM");
        XMLSignature signature = factory.unmarshalXMLSignature(valContext);
        return signature.validate(valContext);
    }
}

サンプル

これでたとえばこんな感じのXMLファイルを

<test>hoge</test>

こんな感じで署名することが出来る。

<?xml version="1.0" encoding="UTF-8" standalone="no"?><test>hoge<Signature xmlns="http://www.w3.org/2000/09/xmldsig#"><SignedInfo><CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments"/><SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/><Reference URI=""><Transforms><Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/></Transforms><DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/><DigestValue>uieJsB59F3qAhTgbESJhvqxI+s7nQSKSt6XIZud2yl8=</DigestValue></Reference></SignedInfo><SignatureValue>cc55XeR9IFXp0OAoUjEiFGbs4ctT5ZLN3HiHoWMX4kdcmnSYlzfmwswXmONgKc2gu4SPlAmYG+MU
EpsJ17UoH3tMcMvO+lmoeoIaXHUVhrXpTw2mYjQ2YKrtxhuQCZpxJmWF3CUVslh57pR53Bki8aaR
hF1bSttgWEmPruOJBWz2PIzUpW1rQn2xvzIaF9MWPnFei0rlD7LulacVYyBxPL/Yjoln5VZANYf5
ivYi2LGQrZUS4Z6fBwo3KswgZNC6+5ENYvjTj3WMDNvNL+cYDFIlZQrQjxymluXzQV5dIjGyrX6D
9U4CuZ6SZJFfMJyjarwkpLErmj8G5i8oHNT19w==</SignatureValue><KeyInfo><KeyValue><RSAKeyValue><Modulus>y7Bio+6VxTzgwS5xcQxPeS1189+XvbDNVVg4D/lRgNsfL6V8gnhABpb/fzilTNu8OIHQ3Dd49JnM
X8dNzUjrgrskvbK+Vs9e4jCZ81KrItIBwXRT2w6WyJI5sNvO+mFbBZYxWS9gBKgSHbx4SMgAQuG4
S80HNfaVtU4cLopt7TjeRT8HS2hCPoddCey7WSdHdG9w/CIFdqPtxJoIhytobgecuaG8YciwixPz
i+YtC0abJ4yyLTw38C1YU1qEbcr5zXKpsQ3ZwtVNIdYonwqc6ilZg5OEDEGYFzJgtBKdPly2/PsU
raeLdI7eJDtI56eZiCjEP+NMZm3vAD4p2pGl1Q==</Modulus><Exponent>AQAB</Exponent></RSAKeyValue></KeyValue></KeyInfo></Signature></test>

電子署名、特にXMLの。(前編)

ここしばらく電子署名まわりのコードを書いてました。 何にどう使ってるかは内緒。

電子署名

「平文と暗号文と公開鍵が分かってる」ってまずくないのかな、とか思って調べてるうちに見付けた記事。

僕も「電子署名はハッシュを秘密鍵で暗号化したものをハッシュと一緒に相手に渡して公開鍵で復号できるかどうかで検証する。」と理解してたので、?となったあと、なるほどなーという感じ。

なお、最初の疑問については、よくよく考えたら公開鍵暗号って電子署名に限らずそういうものでした。

選択平文攻撃 Chosen-plaintext attack (CPA)
任意の平文に対応する暗号文を得られる条件で、暗号文から平文を求める攻撃である。公開鍵暗号の場合には、公開鍵を用いて任意の平文を暗号化できるため、選択平文攻撃に対して安全であることが必須である。 暗号解読 - Wikipedia

GnuPG

PGPGNU版別実装。この記事が分かりやすかった。

@IT:ファイルに電子署名を行うには

gpg --verifyのリターンコードは検証成功時に成功の0、検証失敗時に失敗扱いになるようなので、シェルスクリプトなんかで署名が正しいかどうかなどを検証することもできる。

XML電子署名

署名を付けたいデータがXMLファイルだったので、テキストファイルとして署名を付けてもよかったのだけれど、折角なのでこっちで。学生の頃に色々研究されてた記憶がある。

  • XMLとして同じなら(たとえば<root/><root></root>とか。)、データを変更しても署名が変わらない。(署名する前に正規化などの変換をしている。)
  • 署名したいXMLデータ自身の中に署名を埋め込める。
    • 署名部分を抜き取る仕様が決まっていて、それで署名を抜き取ったあとのデータに対して署名が正しいかどうかを検証する。
    • (元データ+署名で署名付きのデータを配る方法はGPGでもできる。)

Javaが読み書きできる鍵の形式

XMLや暗号まわりについては、Javaはかなりこの辺りのライブラリが標準で充実している。

秘密鍵と公開鍵のペア」というと、日常よく触るのはsshの鍵に使うssh-keygenとかで作るid_rsaとid_rsa.pubのペアで、

ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCxBgSJMzzdZ3aoBtt3Joyn3hlQr81jmRb9cK41LxZSA+U+RKKAop72dbAikCqjedbTBnEQyxT4M2OZuh/yYAPX0iV+E3MfxugBUZJkNPaqawD6kG20nj/HY8bdUWSDfoeEgNa+nelFSUeMMcHBXwqYz6mtQ1h41RrQjDPCe/ZWy0jPUlYRqvvwFvXtTPdR7VIYpVDoe9aVSpZS/nV19uxKpMO+5CTL4I2YFQFqTCv5TnPqr4kBHGzqqn+FWwk2yVd4NTfX28Rpk9XVl6bugPp97SLgDpKYugeQxiwh46VT9WpvGZVqaxiJlSUrZfVqx/YVc2JOr6NudW4P5Z5g1Yb5 kzwmsyk@spellbound.local

たとえば公開鍵がこういうのだったりするのですが、これはOpenSSH形式と呼ばれるもので、独自フォーマット。

PEMとかDERとかP7XとかPFXとか一杯あって正直困るのですが違いはこの辺にまとまってます。

余談だけど、Windowsのコード署名にはpfx形式の鍵を使う。これも最近やった。

で、そんな感じで色々有る中で、Javaのライブラリが読めるのはPKCS#8のDER形式。

OpenSSLを使ってこんな感じで作れる。最初のprivate_key.pemはRSA秘密鍵で、ssh-keygen -t rsa -b 2048が作る秘密鍵と同じ。
秘密鍵から公開鍵を作ってるのは暗号化アルゴリズムRSAだからできるということらしい。(逆は当然非常に難しい。)

# generate a 2048-bit RSA private key
$ openssl genrsa -out private_key.pem 2048
# convert private Key to PKCS#8 format (so Java can read it)
$ openssl pkcs8 -topk8 -inform PEM -outform DER -in private_key.pem -out private_key.der -nocrypt
# output public key portion in DER format (so Java can read it)
$ openssl rsa -in private_key.pem -pubout -outform DER -out public_key.der  

codeartisan: RSA Public Key Cryptography in Javaより)

次はJavaでの実装について。