JavaでXML署名を実装する

JavaでXML署名を実装する必要があったのでまとめておく。
基本的には以下のコードをJava 11で実際に動くように少しいじっただけ。

- <https://docs.oracle.com/javase/6/docs/technotes/guides/security/xmldsig/envelope.xml>
- <https://docs.oracle.com/javase/6/docs/technotes/guides/security/xmldsig/GenEnveloped.java>
- <https://docs.oracle.com/javase/6/docs/technotes/guides/security/xmldsig/Validate.java>

## 環境

- macOS 10.13.6 (High Sierra)
- OpenSSL 1.0.2p
- Java 11.0.1 (OpenJDK)

## 準備：秘密鍵・証明書の作成

RSA秘密鍵・X.509公開鍵証明書のペアを作成
`$ openssl req -out alice.crt -nodes -keyout alice.pem -newkey rsa:4096 -sha256 -x509 -days 365`

証明書の確認

`$ openssl x509 -in alice.crt -noout -text`

Javaで秘密鍵を読み込むには、秘密鍵をPKCS #8・DER形式に変換する必要がある。
`$ openssl pkcs8 -in alice.pem -outform DER -out alice.pk8 -topk8 -nocrypt`

ファイルの状況は次の通り。

- alice.pk8：PKCS #8・DER形式の秘密鍵（RSA 4096 bit）
- alice.crt：X.509公開鍵証明書（RSA・SHA-256）
- alice.pem：PEM形式の秘密鍵（今回は使わない）

## サンプルXMLファイル

```xml:envelope.xml
<!-- ref. https://docs.oracle.com/javase/6/docs/technotes/guides/security/xmldsig/envelope.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<Envelope xmlns="urn:envelope">
</Envelope>
```

## XML署名プログラム

```java:GenEnveloped.java
// ref. https://docs.oracle.com/javase/6/docs/technotes/guides/security/xmldsig/GenEnveloped.java

import javax.xml.crypto.dsig.*;
import javax.xml.crypto.dsig.dom.DOMSignContext;
import javax.xml.crypto.dsig.keyinfo.*;
import javax.xml.crypto.dsig.spec.*;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.OutputStream;
import java.security.*;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.security.interfaces.RSAPrivateKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Collections;
import java.util.List;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.*;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;

import org.w3c.dom.Document;

public class GenEnveloped {
  public static void main(String[] args) {
    if (args.length != 4) {
      System.err.println("Usage: java GenEnveloped [input XML path] [output XML path] [private key path (pk8)] [certificate path]");
      System.exit(1);
    }

    try {
      genEnveloped(args[0], args[1], args[2], args[3]);
    } catch (Exception e) {
      System.err.println(e);
      System.exit(1);
    }
  }

  public static void genEnveloped(String inXmlPath, String outXmlPath, String privateKeyPath, String certPath) throws Exception {
    // Create a DOM XMLSignatureFactory that will be used to generate the
    // enveloped signature
    XMLSignatureFactory fac = XMLSignatureFactory.getInstance("DOM");

    // Create a Reference to the enveloped document (in this case we are
    // signing the whole document, so a URI of "" signifies that) and
    // also specify the SHA256 digest algorithm and the ENVELOPED Transform.
    DigestMethod dm = fac.newDigestMethod(DigestMethod.SHA256, null);
    List<Transform> transforms = Collections.singletonList(fac.newTransform(Transform.ENVELOPED, (TransformParameterSpec) null));
    Reference ref = fac.newReference("", dm, transforms, null, null);

    // Create the SignedInfo
    CanonicalizationMethod cm = fac.newCanonicalizationMethod(CanonicalizationMethod.INCLUSIVE_WITH_COMMENTS, (C14NMethodParameterSpec) null);
    SignatureMethod sm = fac.newSignatureMethod(SignatureMethod.RSA_SHA256, null);
    List<Reference> references = Collections.singletonList(ref);
    SignedInfo si = fac.newSignedInfo(cm, sm, references);

    // Read a RSA private key
    FileInputStream fis = new FileInputStream(privateKeyPath);
    byte[] privateKeyByte = new byte[fis.available()];
    fis.read(privateKeyByte);
    fis.close();
    PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(privateKeyByte);
    KeyFactory kf = KeyFactory.getInstance("RSA");
    RSAPrivateKey privateKey = (RSAPrivateKey) kf.generatePrivate(keySpec);

    // Read a X.509 certificate
    KeyInfoFactory kif = fac.getKeyInfoFactory();
    CertificateFactory cf = CertificateFactory.getInstance("X.509");
    X509Certificate cert = (X509Certificate) cf.generateCertificate(new FileInputStream(certPath));
    X509Data x509Data = kif.newX509Data(Collections.singletonList(cert));

    // Create a KeyInfo and add the X509Data to it
    KeyInfo ki = kif.newKeyInfo(Collections.singletonList(x509Data));

    // Instantiate the document to be signed
    DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
    dbf.setNamespaceAware(true);
    Document doc = dbf.newDocumentBuilder().parse(new FileInputStream(inXmlPath));

    // Create a DOMSignContext and specify the RSA PrivateKey and
    // location of the resulting XMLSignature's parent element
    DOMSignContext dsc = new DOMSignContext(privateKey, doc.getDocumentElement());

    // Create the XMLSignature (but don't sign it yet)
    XMLSignature signature = fac.newXMLSignature(si, ki);

    // Marshal, generate (and sign) the enveloped signature
    signature.sign(dsc);

    // output the resulting document
    OutputStream os = new FileOutputStream(outXmlPath);
    TransformerFactory tf = TransformerFactory.newInstance();
    Transformer trans = tf.newTransformer();
    trans.transform(new DOMSource(doc), new StreamResult(os));
  }
}
```

コンパイル
`$ javac GenEnveloped.java`

Usage
`$ java GenEnveloped [input XML path] [output XML path] [private key path (pk8)] [certificate path]`

実行例
`$ java GenEnveloped envelope.xml envelopedSignature.xml alice.pk8 alice.crt`

## XML署名検証プログラム

```java:Validate.java
// ref. https://docs.oracle.com/javase/6/docs/technotes/guides/security/xmldsig/Validate.java

import javax.xml.crypto.*;
import javax.xml.crypto.dsig.*;
import javax.xml.crypto.dsig.dom.DOMValidateContext;
import javax.xml.crypto.dsig.keyinfo.*;
import java.io.FileInputStream;
import java.security.*;
import java.security.cert.X509Certificate;
import java.util.Iterator;
import javax.xml.parsers.DocumentBuilderFactory;

import org.w3c.dom.Document;
import org.w3c.dom.NodeList;

public class Validate {
  public static void main(String[] args) {
    if (args.length != 1) {
      System.err.println("Usage: java Validate [input XML path]");
      System.exit(1);
    }

    try {
      validate(args[0]);
    } catch (Exception e) {
      System.err.println(e);
      System.exit(1);
    }
  }

  public static boolean validate(String inXmlPath) throws Exception {
    // Instantiate the document to be validated
    DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
    dbf.setNamespaceAware(true);
    Document doc = dbf.newDocumentBuilder().parse(new FileInputStream(inXmlPath));

    // 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) {
      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");
    return coreValidity;
  }

  /**
   * 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;

      for (Object keyInfoContent : keyInfo.getContent()) {
        if (keyInfoContent instanceof X509Data) {
          for (Object x509Content : ((X509Data) keyInfoContent).getContent()) {
            X509Certificate cert = (X509Certificate) x509Content;
            PublicKey pk = cert.getPublicKey();
            // 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("RSA") && algURI.equalsIgnoreCase(SignatureMethod.RSA_SHA256))
        return true;
      else 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;
    }
  }
}
```

コンパイル
`$ javac Validate.java`

Usage
`$ java Validate [input XML path]`

実行例（検証成功）

```
$ java Validate envelopedSignature.xml
Signature passed core validation
```

実行例（検証失敗）

```
$ java Validate envelopedSignature.xml
Signature failed core validation
signature validation status: false
ref[0] validity status: true
```

## P.S.

ソースコードはこちら。
[haru52/xmldsig](https://github.com/haru52/xmldsig)

## References

- [Java XML デジタル署名 API](https://docs.oracle.com/javase/jp/6/technotes/guides/security/xmldsig/XMLDigitalSignature.html)
- <https://docs.oracle.com/javase/6/docs/technotes/guides/security/xmldsig/envelope.xml>
- <https://docs.oracle.com/javase/6/docs/technotes/guides/security/xmldsig/GenEnveloped.java>
- <https://docs.oracle.com/javase/6/docs/technotes/guides/security/xmldsig/Validate.java>
- [JavaのXMLデジタル署名APIを利用してXML署名 - Qiita](https://qiita.com/KevinFQ/items/4e2484a659b618530e72)
- [JavaのXMLデジタル署名APIを利用してXML署名を検証する。 - Qiita](https://qiita.com/KevinFQ/items/24f484de8d51b1cc0b46)