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 ファイルをコードで検証してみました。Java の XML デジタル署名 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