電子署名、特にXMLの。(後編)
JavaでXMLファイルに署名したり検証したりするプログラムの書き方。
必要なものは概ね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>