Apache Santuario ( XML Security ) を使って XML 署名をやってみる

Apache Santuario ( XML Security ) を使って XML 署名をやってみたので、メモしておきます。

Apache Santuario
http://santuario.apache.org/

さっそくいきます。

1. 署名対象の XML ファイルを用意する


■ test.xml

<?xml version="1.0" encoding="UTF-8"?>
<element1>
  <element2 attr="AAA">BBB</element2>
</element1>


2. 秘密鍵、証明書を作成する


XML 署名する際に必要になるので秘密鍵、証明書を作成しておく。

keytool -genkeypair -keysize 2048 -keyalg RSA -sigalg SHA1withRSA -alias test -keystore keystore -storepass password


3. XML 署名するコードを作成する


Santuario 使って XML 署名するサンプルコードが以下になります。

■ XMLSignTest.java

package xml.sign.test;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.UnrecoverableKeyException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

import org.apache.xml.security.Init;
import org.apache.xml.security.signature.XMLSignature;
import org.apache.xml.security.transforms.Transforms;
import org.apache.xml.security.utils.Constants;
import org.apache.xml.security.utils.XMLUtils;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.xml.sax.SAXException;

public class XMLSignTest {
	private static PrivateKey privateKey;
	private static X509Certificate certificate;
	private static PublicKey publicKey;
	
	public static void main(String[] args) throws Exception {
		setPrivateKeyAndCertificate();
		
		XMLSignTest xst = new XMLSignTest();
		Document doc = xst.getXMLDocument(new File("test.xml"));
		Init.init();
		File signFile = new File("sign.xml");
		String BaseURI = signFile.toURI().toURL().toString();
		XMLSignature sig = 
	            new XMLSignature(doc, BaseURI, XMLSignature.ALGO_ID_SIGNATURE_RSA_SHA1);
		
		Element root = doc.getDocumentElement();
		root.appendChild(sig.getElement());
                sig.getSignedInfo().addResourceResolver(
                new OfflineResolver()
                );
        Transforms transforms = new Transforms(doc);
        transforms.addTransform(Transforms.TRANSFORM_ENVELOPED_SIGNATURE);
        transforms.addTransform(Transforms.TRANSFORM_C14N_WITH_COMMENTS);
        sig.addDocument("", transforms, Constants.ALGO_ID_DIGEST_SHA1);
        
        sig.addDocument("http://www.w3.org/TR/xml-stylesheet");
        sig.addDocument("http://www.nue.et-inf.uni-siegen.de/index.html");
        
	sig.addKeyInfo(certificate);
        sig.addKeyInfo(publicKey);
        sig.sign(privateKey);
        
        FileOutputStream f = new FileOutputStream(signFile);
        XMLUtils.outputDOMc14nWithComments(doc, f);
        f.close();
	}

	private Document getXMLDocument(File f) {
		Document document = null;
		try {
			DocumentBuilderFactory factory = DocumentBuilderFactory
					.newInstance();
			DocumentBuilder documentBuilder = factory.newDocumentBuilder();
			document = documentBuilder.parse(f);
		} catch (ParserConfigurationException | IOException | SAXException e) {
			e.printStackTrace();
		}
		return document;
	}

	private static void setPrivateKeyAndCertificate() {
		String ksType = "JKS";
		String keyStoreFile = "keystore";
		String keyStorePass = "password";
		String alias = "test";
		String privateKeyPass = "password";
		try {
			KeyStore ks = KeyStore.getInstance(ksType);
			ks.load(new FileInputStream(keyStoreFile), keyStorePass.toCharArray());

			privateKey = (PrivateKey) ks.getKey(alias, privateKeyPass.toCharArray());
			certificate = (X509Certificate) ks.getCertificate(alias);
			publicKey = certificate.getPublicKey();
		} catch (CertificateException | UnrecoverableKeyException
				| KeyStoreException | NoSuchAlgorithmException | IOException e) {
			e.printStackTrace();
		}
	}
}

※ 例外処理とかグチャグチャですが、ご了承ください・・・

Santuario のリポジトリにサンプルコードがあるので、こいつを参考にしております。

・CreateSignature.java
https://svn.apache.org/repos/asf/santuario/xml-security-java/trunk/samples/org/apache/xml/security/samples/signature/CreateSignature.java

サンプルコードの OfflineResolver もリポジトリにありますが、そのままだとコンパイルエラーになるので、ちょっと修正を加えてます。

・OfflineResolver.java
https://svn.apache.org/repos/asf/santuario/xml-security-java/trunk/samples/org/apache/xml/security/samples/utils/resolver/OfflineResolver.java

修正を加えたコードが以下になります。

/**
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements. See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership. The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License. You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied. See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package xml.sign.test;

import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.HashMap;
import java.util.Map;

import org.apache.xml.security.signature.XMLSignatureInput;
import org.apache.xml.security.utils.resolver.ResourceResolverContext;
import org.apache.xml.security.utils.resolver.ResourceResolverException;
import org.apache.xml.security.utils.resolver.ResourceResolverSpi;
import org.w3c.dom.Attr;

/**
 * This class helps us home users to resolve http URIs without a network
 * connection
 *
 * @author $Author: giger $
 */
public class OfflineResolver extends ResourceResolverSpi {

	static org.slf4j.Logger log = org.slf4j.LoggerFactory
			.getLogger(OfflineResolver.class);

	/** Field _uriMap */
	static Map<String, String> _uriMap = null;

	/** Field _mimeMap */
	static Map<String, String> _mimeMap = null;

	static {
		org.apache.xml.security.Init.init();

		OfflineResolver._uriMap = new HashMap<String, String>();
		OfflineResolver._mimeMap = new HashMap<String, String>();

		OfflineResolver.register("http://www.w3.org/TR/xml-stylesheet",
				"samples/data/org/w3c/www/TR/xml-stylesheet.html", "text/html");
		OfflineResolver
				.register("http://www.w3.org/TR/2000/REC-xml-20001006",
						"samples/data/org/w3c/www/TR/2000/REC-xml-20001006",
						"text/xml");
		OfflineResolver.register(
				"http://www.nue.et-inf.uni-siegen.de/index.html",
				"samples/data/org/apache/xml/security/temp/nuehomepage",
				"text/html");
		OfflineResolver
				.register(
						"http://www.nue.et-inf.uni-siegen.de/~geuer-pollmann/id2.xml",
						"samples/data/org/apache/xml/security/temp/id2.xml",
						"text/xml");
		OfflineResolver.register(
				"http://xmldsig.pothole.com/xml-stylesheet.txt",
				"samples/data/com/pothole/xmldsig/xml-stylesheet.txt",
				"text/xml");
		OfflineResolver
				.register(
						"http://www.w3.org/Signature/2002/04/xml-stylesheet.b64",
						"samples/data/ie/baltimore/merlin-examples/merlin-xmldsig-twenty-three/xml-stylesheet.b64",
						"text/plain");
	}

	/**
	 * Method engineResolve
	 *
	 * @param uri
	 * @param BaseURI
	 *
	 * @throws ResourceResolverException
	 */
	public XMLSignatureInput engineResolve(Attr uri, String BaseURI)
			throws ResourceResolverException {
		String URI = uri.getNodeValue();
		try {
			if (OfflineResolver._uriMap.containsKey(URI)) {
				String newURI = OfflineResolver._uriMap.get(URI);

				log.debug("Mapped " + URI + " to " + newURI);

				InputStream is = new FileInputStream(newURI);

				log.debug("Available bytes = " + is.available());

				XMLSignatureInput result = new XMLSignatureInput(is);

				// XMLSignatureInput result = new
				// XMLSignatureInput(inputStream);
				result.setSourceURI(URI);
				result.setMIMEType((String) OfflineResolver._mimeMap.get(URI));

				return result;
			} else {
				Object exArgs[] = { "The URI " + URI
						+ " is not configured for offline work" };

				throw new ResourceResolverException("generic.EmptyMessage", exArgs, URI, BaseURI);
			}
		} catch (IOException ex) {
			throw new ResourceResolverException("generic.EmptyMessage", ex, URI, BaseURI);
		}
	}

	/**
	 * We resolve http URIs <I>without</I> fragment...
	 *
	 * @param uri
	 * @param BaseURI
	 *
	 */
	public boolean engineCanResolve(Attr uri, String BaseURI) {

		String uriNodeValue = uri.getNodeValue();
		if (uriNodeValue.equals("") || uriNodeValue.startsWith("#")) {
			return false;
		}

		try {
			URI uriNew = getNewURI(uri.getNodeValue(), BaseURI);
			if (uriNew.getScheme().equals("http")) {
				log.debug("I state that I can resolve " + uriNew.toString());
				return true;
			}

			log.debug("I state that I can't resolve " + uriNew.toString());
		} catch (URISyntaxException ex) {
			//
		}

		return false;
	}

	/**
	 * Method register
	 *
	 * @param URI
	 * @param filename
	 * @param MIME
	 */
	private static void register(String URI, String filename, String MIME) {
		OfflineResolver._uriMap.put(URI, filename);
		OfflineResolver._mimeMap.put(URI, MIME);
	}

	private static URI getNewURI(String uri, String baseURI)
			throws URISyntaxException {
		URI newUri = null;
		if (baseURI == null || "".equals(baseURI)) {
			newUri = new URI(uri);
		} else {
			newUri = new URI(baseURI).resolve(uri);
		}

		// if the URI contains a fragment, ignore it
		if (newUri.getFragment() != null) {
			URI uriNewNoFrag = new URI(newUri.getScheme(),
					newUri.getSchemeSpecificPart(), null);
			return uriNewNoFrag;
		}
		return newUri;
	}

	@Override
	public boolean engineCanResolveURI(ResourceResolverContext arg0) {
		// TODO Auto-generated method stub
		return false;
	}

	@Override
	public XMLSignatureInput engineResolveURI(ResourceResolverContext arg0)
			throws ResourceResolverException {
		// TODO Auto-generated method stub
		return null;
	}
}

※ 変更箇所は
・ResourceResolverException の引数
・engineCanResolveURI、engineResolveURI が抽象メソッドになっているので空実装を追加
です。

サンプルコードを実行すると XML 署名されたファイルが生成されます。DigestValue やら SignatureValue 等々、いろいろな情報が付与されてますね。

■ sign.xml

<element1>
  <element2 attr="AAA">BBB</element2>
<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
<ds:SignedInfo>
<ds:CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"></ds:CanonicalizationMethod>
<ds:SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"></ds:SignatureMethod>
<ds:Reference URI="">
<ds:Transforms>
<ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"></ds:Transform>
<ds:Transform Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments"></ds:Transform>
</ds:Transforms>
<ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"></ds:DigestMethod>
<ds:DigestValue>yTlPkCUuKldflrw2FXx4VrbnBEU=</ds:DigestValue>
</ds:Reference>
<ds:Reference URI="http://www.w3.org/TR/xml-stylesheet">
<ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"></ds:DigestMethod>
<ds:DigestValue>pABdjWJGeIB7U4NdYaZwMfhGCfQ=</ds:DigestValue>
</ds:Reference>
<ds:Reference URI="http://www.nue.et-inf.uni-siegen.de/index.html">
<ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"></ds:DigestMethod>
<ds:DigestValue>fj1YaIQBEjHK2ws83kh21Cak93Q=</ds:DigestValue>
</ds:Reference>
</ds:SignedInfo>
<ds:SignatureValue>
IfJ3CYQT5l2p5jjpGvBGlO1ZgP1oKKdeATvZUkqoWe7C8J1Ip5UxkrdvvWl9j7x61Hpm0qGjNZhn
/nsbK9cmzNeVgZZDh6MRrFl3QqoXZXJAPkZ3R1hyApoUbZ/4e7Dd4LZO24k2Lke9cyQWNxcdZLJq
UrvaJiI8vAOxEez+bKyInYNPBzLOJT4seSRQP9qc8OFzJeL7+LFTPK7K0jlczuz6VdzKhxlmH9Dq
1Y4ot39FKyYG2fjJNNzQ4Ikgikzytc/UF5xzJ4u0RJ3HnUtbXjsnW7tsvGftsqwBLfBRwe51DxvA
KzQUp+KRpC3zuAgg6/GhHCMbs5z07l5xb+M4ZQ==
</ds:SignatureValue>
<ds:KeyInfo>
<ds:X509Data>
<ds:X509Certificate>
MIIDTzCCAjegAwIBAgIEP9Qe1jANBgkqhkiG9w0BAQUFADBYMQswCQYDVQQGEwJKUDENMAsGA1UE
CBMEdGVzdDENMAsGA1UEBxMEdGVzdDENMAsGA1UEChMEdGVzdDENMAsGA1UECxMEdGVzdDENMAsG
A1UEAxMEdGVzdDAeFw0xNTA1MjAxMzMyMzVaFw0xNTA4MTgxMzMyMzVaMFgxCzAJBgNVBAYTAkpQ
MQ0wCwYDVQQIEwR0ZXN0MQ0wCwYDVQQHEwR0ZXN0MQ0wCwYDVQQKEwR0ZXN0MQ0wCwYDVQQLEwR0
ZXN0MQ0wCwYDVQQDEwR0ZXN0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAmSsfiiJv
aqq7lqhyfXkMMIkBSldlJDMnWdnKzjHTAm3qi1QrGhXQJH4BGJcZI2Sm5K6mEjSD4apW3lerTWap
6t7bhqMe3VnZkuImNW8kbJThdeePlFWxaDwXkzoDwVqoSWUhKI4CEFvboSO0LSE2QpTWxXvLpluX
cR8nqMDgPqTnIfYgmw9gBq15BGCWRxEyya2VR+jsz/tfxIcs+KEKJwUW3xulbQQtNHkySyDg2LFy
70e1aOsdjk7snyuzq/QKBRE46XxCAxNk5Iknbj3ma9t+CdWmVBS2+XyUgrO+m6fpOmC8mtiss6zd
lDi7lqLQWAicHNQefAy4dBCnOHnUcQIDAQABoyEwHzAdBgNVHQ4EFgQUgZZO1o44hD0BtXWOWRa9
DYYJ1okwDQYJKoZIhvcNAQEFBQADggEBAGqA1wRJYFgc5Hknr67lJLw4Gbeh5EXbndjD1Yo48u0O
EvGjwEgxBXTqJcpc5lGWhRdFIRIFqsEJC2eftAz/jQxUq+Y4l2ExqaIG1Xxt7/G0cntaqd/HscVq
dhTScnDtFDcPatHPb0c4Y75xY7aKxBhkzxQpjBoNP8P5f2LS1Oi23cUh4niF27HVmJPzqqGBeI0r
zEy0Ogem0ta8kC+pFIT354/DC5IQSzqYkWMqbUukQcOnPZ7mJfNBEEDE3dzQLRVzf+EA2Fgd14dF
5xtb9JmAYEukt4TyjBcPlltFqpOmeZ/8NiigrIyeD7nvVBDpUZcpCtJ1CwPMIzpa+CZDahI=
</ds:X509Certificate>
</ds:X509Data>
<ds:KeyValue>
<ds:RSAKeyValue>
<ds:Modulus>
mSsfiiJvaqq7lqhyfXkMMIkBSldlJDMnWdnKzjHTAm3qi1QrGhXQJH4BGJcZI2Sm5K6mEjSD4apW
3lerTWap6t7bhqMe3VnZkuImNW8kbJThdeePlFWxaDwXkzoDwVqoSWUhKI4CEFvboSO0LSE2QpTW
xXvLpluXcR8nqMDgPqTnIfYgmw9gBq15BGCWRxEyya2VR+jsz/tfxIcs+KEKJwUW3xulbQQtNHky
SyDg2LFy70e1aOsdjk7snyuzq/QKBRE46XxCAxNk5Iknbj3ma9t+CdWmVBS2+XyUgrO+m6fpOmC8
mtiss6zdlDi7lqLQWAicHNQefAy4dBCnOHnUcQ==
</ds:Modulus>
<ds:Exponent>AQAB</ds:Exponent>
</ds:RSAKeyValue>
</ds:KeyValue>
</ds:KeyInfo>
</ds:Signature></element1>


4. 署名された XML ファイルを検証してみる


せっかくなので、署名された XML ファイルをコードで検証してみました。JavaXML デジタル署名 API ( JSR 105 ) 使ったコードになります。

・Validate.java
http://docs.oracle.com/javase/8/docs/technotes/guides/security/xmldsig/Validate.java

XML Digital Signature API Specification (JSR 105)
http://docs.oracle.com/javase/8/docs/technotes/guides/security/xmldsig/overview.html

Validate.java で上記で生成した sign.xml を検証してみると「Signature passed core validation」と出力されるので、一応、ちゃんと署名できてるってことでいいかな。

※ 本当は検証も Santuario 使ってやろうと思ったのですが、なんとなく以下のサンプルコードがイマイチな気がしたので、Java 側の API 使うことにしました・・・

・VerifySignature.java
https://svn.apache.org/repos/asf/santuario/xml-security-java/trunk/samples/org/apache/xml/security/samples/signature/VerifySignature.java


しかし、難しいですね XML 署名。正直、書いた内容もとりあえず動かせたって程度で、理解って部分はほとんどできてません・・・とりあえず以下の記述あたりから勉強ですかね。

Webサービスのセキュリティ(2):XMLデジタル署名とXML暗号 - @IT
http://www.atmarkit.co.jp/ait/articles/0207/24/news001.html

以上です。

[ 環境情報 ]
Windows 7 SP1
Java SE 8 Update 25
Apache Santuario 2.0.4