読者です 読者をやめる 読者になる 読者になる

電子署名、特に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>