Canonicalize XML in Java

はじめに

XML 署名の前に正規化 canonicalization を押さえておくべきだったな、とちょっと反省。

日頃使う html あたりを思い浮かべてもらえればピンとくると思うのだが、同じ内容を表現するのに元のコードは表記上はブレがある。
例えば、<tag> を <tag > と書こうが、ブラウザのパーサーは同じものと解釈する。
しかし、XML のデータ(構造)を丸ごと暗号化するような場合、これでは困る。
異なる数値データになってしまうからだ。
そこで、暗号化する前に何らかの仕方で表記のブレを統一しておきましょうという話になる。
これを正規化 canonicalization と呼んでいて、いくつかの方法が提案されている。

なお、よく C14N という表記が見られるが、これは anonicalizatio が14文字であることに起因する。

公開鍵基盤あたりの普及で電子データの長期保存のニーズは高まるだろうから、今後、この分野の知見は重要分野になってくるのは間違いない。

・・・とは思うのですが、この分野、ネット上の情報の質がちょっと・・・・。

例えば、この記事と同タイトルの英文記事があるのだが、そのサンプルがね。

普通に動かない(笑)。

説明はわかりやすいんすよ。

From the output, we can see that the Root node is removed in the canonicalized data, this is because the NodeFilter in NodeSetDataImpl has filtered this node. Next, the second my:Node has the xmlns:my node before Id node in the canonicalized form. This is based on the Canonical XML specification where the nodes should be in lexical order

出力から、正規化されたデータでルート ノードが削除されていることがわかります。これは、NodeSetDataImpl の NodeFilter がこのノードをフィルター処理したためです。次に、2 番目の my:Node には、正規化された形式で Id ノードの前に xmlns:my ノードがあります。これは、ノードが字句順に配置される必要がある Canonical XML 仕様に基づいています。

ここまで明快に具体的な正規化の方法を言及した記事はほとんどないっすから。
しかし、なんで(おそらくこの人の環境でしか動かない) tool なんてメソッドがソースに混入してるんだ???

車輪の再整備

よくわからない tool は、機能を読み取って変更。

オクテットストリームも InputStream でキャスト。

・・・

諸々する。

(ソースコード公開は少々お待ちを)

正規化例

修正して動かすと例えば、こんな結果が得られる。

何がどう変わったか?

<info/> → <info></info> あたりはわかりやすいでしょう。

よく見ると属性も変化してます。

要チェック。

その他

電子処方箋でも XML の正規化は必要になってきます。
このスレなど参照。

(続く)

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 を眺めてなんか納得。

JCA -Java Cryptography Architecture-

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

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

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

 

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


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

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

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

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


 

 

 

(適宜情報追加予定)