Java XML 署名 API の使い方 -公式サンプル-

署名編の評判が良いようなので、次は検証(Validation)編。

と思ったのだが、オラクル公式サンプルが興味深かったので再掲。
レビューしてみる。

そのコードはこうだ。

import javax.xml.crypto.*;
import javax.xml.crypto.dsig.*;
import javax.xml.crypto.dom.*;
import javax.xml.crypto.dsig.dom.DOMValidateContext;
import javax.xml.crypto.dsig.keyinfo.*;
import java.io.FileInputStream;
import java.security.*;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import javax.xml.parsers.DocumentBuilderFactory;
import org.w3c.dom.Document;
import org.w3c.dom.NodeList;

/**
 * This is a simple example of validating an XML 
 * Signature using the JSR 105 API. It assumes the key needed to
 * validate the signature is contained in a KeyValue KeyInfo. 
 */
public class Validate {

    //
    // Synopsis: java Validate [document]
    //
    //	  where "document" is the name of a file containing the XML document
    //	  to be validated.
    //
    public static void main(String[] args) throws Exception {

	// Instantiate the document to be validated
	DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
	dbf.setNamespaceAware(true);
	Document doc =
            dbf.newDocumentBuilder().parse(new FileInputStream(args[0]));

	// Find Signature element
	NodeList nl = 
	    doc.getElementsByTagNameNS(XMLSignature.XMLNS, "Signature");
	if (nl.getLength() == 0) {
	    throw new Exception("Cannot find Signature element");
	}

	// Create a DOM XMLSignatureFactory that will be used to unmarshal the 
	// document containing the XMLSignature 
	XMLSignatureFactory fac = XMLSignatureFactory.getInstance("DOM");

	// Create a DOMValidateContext and specify a KeyValue KeySelector
        // and document context
	DOMValidateContext valContext = new DOMValidateContext
	    (new KeyValueKeySelector(), nl.item(0));
	
	// unmarshal the XMLSignature
	XMLSignature signature = fac.unmarshalXMLSignature(valContext);

	// Validate the XMLSignature (generated above)
	boolean coreValidity = signature.validate(valContext); 

	// Check core validation status
	if (coreValidity == false) {
    	    System.err.println("Signature failed core validation"); 
	    boolean sv = signature.getSignatureValue().validate(valContext);
	    System.out.println("signature validation status: " + sv);
	    // check the validation status of each Reference
	    Iterator i = signature.getSignedInfo().getReferences().iterator();
	    for (int j=0; i.hasNext(); j++) {
		boolean refValid = 
		    ((Reference) i.next()).validate(valContext);
		System.out.println("ref["+j+"] validity status: " + refValid);
	    }
	} else {
    	    System.out.println("Signature passed core validation");
	}
    }

    /**
     * KeySelector which retrieves the public key out of the
     * KeyValue element and returns it.
     * NOTE: If the key algorithm doesn't match signature algorithm,
     * then the public key will be ignored.
     */
    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;
            List list = keyInfo.getContent();

            for (int i = 0; i < list.size(); i++) {
		XMLStructure 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!");
	}

        //@@@FIXME: this should also work for key types other than DSA/RSA
	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; }
    }
}

このコード自体はそんなにおかしくないと思うかもしれないが、対象となる xml ファイルも掲示されていてそれはこういうものだ。

<Envelope xmlns="urn:envelope">
<Signature xmlns="http://www.w3.org/2000/09/xmldsig#">
<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
<CanonicalizationMethod xmlns="http://www.w3.org/2000/09/xmldsig#" Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments"/>
<SignatureMethod xmlns="http://www.w3.org/2000/09/xmldsig#" Algorithm="http://www.w3.org/2000/09/xmldsig#dsa-sha1"/>
<Reference xmlns="http://www.w3.org/2000/09/xmldsig#" URI="">
<Transforms xmlns="http://www.w3.org/2000/09/xmldsig#">
<Transform xmlns="http://www.w3.org/2000/09/xmldsig#" Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
</Transforms>
<DigestMethod xmlns="http://www.w3.org/2000/09/xmldsig#" Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
<DigestValue xmlns="http://www.w3.org/2000/09/xmldsig#">uooqbWYa5VCqcJCbuymBKqm17vY=</DigestValue>
</Reference>
</SignedInfo>
<SignatureValue xmlns="http://www.w3.org/2000/09/xmldsig#">eO7K1BdC0kzNvr1HpMf4hKoWsvl+oI04nMw55GO+Z5hyI6By3Oihow==</SignatureValue>
<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
<KeyValue xmlns="http://www.w3.org/2000/09/xmldsig#">
<DSAKeyValue xmlns="http://www.w3.org/2000/09/xmldsig#">
<P xmlns="http://www.w3.org/2000/09/xmldsig#">/KaCzo4Syrom78z3EQ5SbbB4sF7ey80etKII864WF64B81uRpH5t9jQTxeEu0ImbzRMqzVDZkVG9 xD7nN1kuFw==</P>
<Q xmlns="http://www.w3.org/2000/09/xmldsig#">li7dzDacuo67Jg7mtqEm2TRuOMU=</Q>
<G xmlns="http://www.w3.org/2000/09/xmldsig#">Z4Rxsnqc9E7pGknFFH2xqaryRPBaQ01khpMdLRQnG541Awtx/XPaF5Bpsy4pNWMOHCBiNU0Nogps QW5QvnlMpA==</G>
<Y xmlns="http://www.w3.org/2000/09/xmldsig#">OqFi0sGpvroi6Ut3m154QNWc6gavH3j2ZoRPDW7qVBbgk7XompuKvZe1owz0yvxq+1K+mWbL7ST+ t5nr6UFBCg==</Y>
</DSAKeyValue>
</KeyValue>
</KeyInfo>
</Signature>
</Envelope>

これを NetBeans あたりで実行すると(ちなみに Java17 あたりでもほとんど手直しなしで動く)エラーが出てきて、そのメッセージはなかなか興味深い。

Exception in thread "main" javax.xml.crypto.MarshalException: It is forbidden to use algorithm http://www.w3.org/2000/09/xmldsig#dsa-sha1 when secure validation is enabled

要するに dsa-sha1 が現在ではセキュリティ的に問題あるので使えませんよ、と警告してくれるわけだ。

また、Signature 要素を探すとき doc.getElementsByTagNameNS メソッドを使っていて、なんでだ?と訝しんでいたのだが、対象となる xml を眺めてなんか納得。

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 なため具体的な実装はなく、やはり暗号化ブロバイダあたりの機能を使っているってことかな。


 

 

 

(適宜情報追加予定)

 

WebAssembly -JavaScript と C/C++ の連携-

このところ JavaScript で C/C++ を使う方法を調べている。

いくつか方法があるようなのだが、それなりに感動したのは WebAssembly 。

C/C++ ソースから、なんらかの仕方で(ブラウザが扱える)wasm ファイルを生成し、それをブラウザ環境で動作させる、ということらしい。

こう書くとなんやら難しそうに見えるかもしれないが、導入自体はかなり簡単。

MDN のページあたりを参照。

C → wasm 変換はツールがいくつかあるらしいのだが、ワイは emscripten を使った。

emscripten 導入も MacOS なら homebrew を使って

brew install emscripten

で完了。

適当なウェブサーバーに生成された hello.html を配置すると以下のようなページが現れる。

ちゃんとハロワしてますね。

 

もうちょっと習得を進めたい場合は、この記事あたり参照。