JavaでXMLに署名するには、Apache Santuario(XML Security) がデファクトのようです。
1. 証明書と秘密鍵を準備する
まずは、署名するためには証明書と秘密鍵が必要です。事前に準備しておいてください。秘密鍵をpemで用意したら認識されなかったので、秘密鍵はpkcs#8の形式にしておきます。pemを直接読む方法もあるかもしれませんが、ここでは鍵を変換します。
$ openssl pkcs8 -in xxx.pem -topk8 -nocrypt -outform DER -out mypriv.key
2. 証明書と秘密鍵の読み込み
証明書と秘密鍵は次のように読み込みます。あっ、コードはlombokも使ってます。
public static X509Certificate loadCert(InputStream certFile) throws CertificateException {
CertificateFactory cf = CertificateFactory.getInstance("X.509");
return (X509Certificate)cf.generateCertificate(certFile);
}
public static PrivateKey loadPrivateKey(InputStream privateKey) throws IOException, GeneralSecurityException {
val buf = readPrivateKey(privateKey);
val keySPec = new PKCS8EncodedKeySpec(buf);
return KeyFactory.getInstance("RSA").generatePrivate(keySPec);
}
private static byte[] readPrivateKey(InputStream in) throws IOException {
byte[] data = new byte[in.available()];
in.read(data);
return data;
}
3. 署名する
証明書と秘密鍵の準備ができたところで、XMLに署名します。
最初に署名するためのオブジェクトを作ります。オブジェクトを作る時に処理するXMLのDocumentオブジェクトとそのファイルへURI、署名のアルゴリズムを指定します。そして、その後に、オリジナルのXMLに署名するための要素を追加しています。
val sig = new XMLSignature(doc, "file:/test.xml", XMLSignature.ALGO_ID_SIGNATURE_RSA_SHA256);
doc.getDocumentElement().appendChild(sig.getElement())
ちなみにtest.xml
<?xml version="1.0" encoding="UTF-8"?>
<elem1>
<elemt2>xxx</elem2>
</elem1>
次にResourceResolverを登録します。これは、ResourceResolverはこの後、ダイジェストするための要素や別のファイルのコンテンツを取得するためのオブジェクトです。ResourceResolverのコードはあとで説明します。
そのあと、transformsをするためのオブジェクトを設定しています。ResourceResolverはコンテンツをバイト列として取得するだけの責務です。XMLの要素からダイジェストを取得するためには、canonicalizeが必要になります。バイト列からXMLを解析してcanonicalizeするような人がtransfomrsです。
XMLの署名で最初に疑問に思ったのが、どうやって正規化するか、正規化しないとダイジェストが正しく取れないじゃないか、だったのですが、これはCanonical XMLとして標準化されていて、その責務をtransfomrsが担ってくれます。
このtransformsをaddDocumentでuriと共に指定します。変換が不要なファイルなどの場合は、nullを指定します。
sig.getSignedInfo().addResourceResolver(new MyResolver());
val transforms = new Transforms(kouseiBase);
transforms.addTransform(Transforms.TRANSFORM_ENVELOPED_SIGNATURE);
transforms.addTransform(Transforms.TRANSFORM_C14N_OMIT_COMMENTS);
sig.addDocument("#elem2", transforms, MessageDigestAlgorithm.ALGO_ID_DIGEST_SHA256);
sig.addDocument("somefile.txt", null, MessageDigestAlgorithm.ALGO_ID_DIGEST_SHA256);
あとは、証明書、秘密鍵を指定して署名します。これで最初のdocに署名情報が付加されています。
```java`
sig.addKeyInfo(certificate);
sig.addKeyInfo(certificate.getPublicKey());
sig.sign(privateKey);
## 4. ResourceResolverSpi
URIの解決にはREsourceResolverSpiで行います。実装すべきメソッドはengineCanResolveURIとengineResolveURIの二つです。前者はaddDocumentで指定したURIがこのresolverで解決できるかどうかをチェックします。後者はそのuriからコンテンツを取得するためのメソッドです。XMLSignatureInputというオブジェクトを返します。
例外が発生したらResourceResolverExceptionを投げるのですが、正しい作り方がわからないです。ここのコードのコンストラクタはdeprecatedになっています。
```java
class MyResolver extends ResourceResolverSpi {
@override
public XMLSignatureInput engineResolveURI(ResourceResolverContext context) throws ResourceResolverException {
val uri = context.attr.getValue();
try {
if (uri.equals("#elem2")) {
val result = new XMLSignatureInput(new ByteArrayInputStream("<elemt2>xxx</elem2>".getBytes("utf-8")));
result.setSourceURI(uri);
result.setMIMEType("text/xml");
return result;
}
if (uri.equals("somefile.txt")) {
val result = new XMLSignatureInput(new ByteArrayInputStream(new FileInputStream(uri)));
result.setSourceURI(uri);
result.setMIMEType("application/octet-stream");
return result;
}
} catch (Exception e) {
throw new ResourceResolverException("generic.EmptyMessage", e, uri, context.baseUri);
}
return null;
}
@override
public boolean engineCanResolveURI(ResourceResolverContext context) {
val uri = context.attr.getValue();
if (uri.equals("#elem2")) return true;
if (uri.equals("somefile.txt")) return true;
return false;
}
}
5. おわり
ざっとこんな感じのコードでXMLの署名が行えます。XMLの署名はかなりややこしいですが、ライブラリを使えばわりと簡単に署名が行えます。ただ、ドキュメントが少ないような…。
でわでわ