gfan
gfan

Reputation: 1099

Signature validation failed after formatting an XML with SignatureValue on it

From apache santuario xmlsignature test, I do a test: add a signature to the XML file, then save it as a file, after that read it from that file, and verify the signature. Please refer to the comments in the code.

The plain xml file is :

<?xml version="1.0" encoding="UTF-8"?>
<PurchaseOrder xmlns="urn:example:po">
  <Items>
    <Item Code="001-001-001" Quantity="1">
      spade
    </Item>
    <Item Code="001-001-002" Quantity="1">
      shovel
    </Item>
  </Items>
  <ShippingAddress>
    Dig PLC, 1 First Ave, Dublin 1, Ireland
  </ShippingAddress>
  <PaymentInfo>
    <BillingAddress>
      Dig PLC, 1 First Ave, Dublin 1, Ireland
    </BillingAddress>
    <CreditCard Type="Amex">
      <Name>Foo B Baz</Name>
      <Number>1234 567890 12345</Number>
      <Expires Month="1" Year="2005" />
    </CreditCard>
  </PaymentInfo>
</PurchaseOrder>

I have some questions:

  1. Verify the signature by using the cert in the XML. If it works, the XML file is self-interpreted, why do we still need a cert loaded from the Keystore?
  2. I can't format the generated XML which contains the signature. If I change some piece of the XML file, it will cause the verification to fail. How should I format the XML file? Or just leave it there?
      <ds:SignedInfo><!--If I add a lot of blank spaces at the start of this line, the verifying is ok-->
<ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"></ds:CanonicalizationMethod><!--If I add a blank space at the start of this line, the verifying is not working-->

The main Java Code:

import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.security.Key;
import java.security.KeyStore;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.List;
import javax.xml.namespace.QName;
import org.apache.xml.security.signature.XMLSignature;
import org.junit.Assert;
import org.w3c.dom.Document;
import org.apache.xml.security.utils.XMLUtils;
import org.w3c.dom.Element;

/**
 * This tests using the DOM API of Apache Santuario - XML Security for Java for XML Signature.
 */
public class SignatureDOMTest extends org.junit.Assert {

    // Sign + Verify an XML Document using the DOM API
    @org.junit.Test
    public void testSignatureUsingDOMAPI() throws Exception {

        // Read in plaintext document
        InputStream sourceDocument =
                new FileInputStream(new File("plaintext.xml"));
        Document document = XMLUtils.read(sourceDocument, true);

        // Set up the Key
        KeyStore keyStore = KeyStore.getInstance("jks");
        keyStore.load(
            this.getClass().getClassLoader().getResource("clientstore.jks").openStream(),
            "cspass".toCharArray()
        );
        Key key = keyStore.getKey("myclientkey", "ckpass".toCharArray());
        X509Certificate cert = (X509Certificate)keyStore.getCertificate("myclientkey");

        // Sign using DOM
        List<QName> namesToSign = new ArrayList<QName>();
        namesToSign.add(new QName("urn:example:po", "PaymentInfo"));

        SignatureUtils.signUsingDOM(
            document, namesToSign, "http://www.w3.org/2000/09/xmldsig#rsa-sha1", key, cert
        );
        // Verify using DOM, here it use the cert load from Keystore.
        SignatureUtils.verifyUsingDOM(document, namesToSign, cert);

        // Those code are my test:
        // I write the signed xml to a file, then read from the file.
        XMLUtils.outputDOM(document, new FileOutputStream(new File("plaintext_signed.xml")));
        
        // read from the signed file.
        Document signedDocument = XMLUtils.read(new FileInputStream(new File("plaintext_signed.xml")), true);

        // Verify using DOM
        List<QName> namesToSign1 = new ArrayList<QName>();
        namesToSign1.add(new QName("urn:example:po", "PaymentInfo"));

        Element sigElement = SignatureUtils.getSignatureElement(signedDocument);
        Assert.assertNotNull(sigElement);

        SignatureUtils.findElementsToVerify(signedDocument, namesToSign1);
        
        // try to get cert from the node in the xml.
        XMLSignature signature = new XMLSignature(sigElement, "");

        X509Certificate cert_in_xml = signature.getKeyInfo().getX509Certificate();
        
        // verify the signature by using the cert in the xml.
        // If it works, why do we need a cert loaded from the keystore?
        SignatureUtils.verifyUsingDOM(signedDocument, namesToSign1, cert_in_xml);
    }
}

Include functions in SignatureUtils.java for integrity:

    
import org.apache.xml.security.Init;
import org.apache.xml.security.keys.KeyInfo;
import org.apache.xml.security.signature.XMLSignature;
import org.apache.xml.security.transforms.Transforms;
import org.junit.Assert;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import javax.xml.namespace.QName;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;
import java.security.Key;
import java.security.cert.X509Certificate;
import java.util.List;
import java.util.UUID;

/**
 * Some utility methods for signing/verifying documents
 */
public final class SignatureUtils {

    static {
        Init.init();
    }

    private SignatureUtils() {
        // complete
    }

    /**
     * Sign the document using the DOM API of Apache Santuario - XML Security for Java.
     * It signs a list of QNames that it finds in the Document via XPath.
     */
    public static void signUsingDOM(
        Document document,
        List<QName> namesToSign,
        String algorithm,
        Key signingKey,
        X509Certificate signingCert
    ) throws Exception {
        XMLSignature sig =
            new XMLSignature(document, "", algorithm, "http://www.w3.org/2001/10/xml-exc-c14n#");
        Element root = document.getDocumentElement();
        root.appendChild(sig.getElement());

        XPathFactory xpf = XPathFactory.newInstance();
        XPath xpath = xpf.newXPath();
        xpath.setNamespaceContext(new DSNamespaceContext());

        for (QName nameToSign : namesToSign) {
            String expression = "//*[local-name()='" + nameToSign.getLocalPart() + "']";
            NodeList elementsToSign =
                    (NodeList) xpath.evaluate(expression, document, XPathConstants.NODESET);
            for (int i = 0; i < elementsToSign.getLength(); i++) {
                Element elementToSign = (Element)elementsToSign.item(i);
                Assert.assertNotNull(elementToSign);
                String id = UUID.randomUUID().toString();
                elementToSign.setAttributeNS(null, "Id", id);
                elementToSign.setIdAttributeNS(null, "Id", true);

                Transforms transforms = new Transforms(document);
                transforms.addTransform("http://www.w3.org/2001/10/xml-exc-c14n#");
                sig.addDocument("#" + id, transforms, "http://www.w3.org/2000/09/xmldsig#sha1");
            }
        }

        sig.sign(signingKey);

        // Find the Signature Element
        Element sigElement = getSignatureElement(document);
        Assert.assertNotNull(sigElement);

        if (signingCert != null) {
            sig.addKeyInfo(signingCert);
        }
    }

    /**
     * Verify the document using the DOM API of Apache Santuario - XML Security for Java.
     * It finds a list of QNames via XPath and uses the DOM API to mark them as having an
     * "Id".
     */
    public static void verifyUsingDOM(
        Document document,
        List<QName> namesToSign,
        X509Certificate cert
    ) throws Exception {
        // Find the Signature Element
        Element sigElement = getSignatureElement(document);
        Assert.assertNotNull(sigElement);

        findElementsToVerify(document, namesToSign);

        XMLSignature signature = new XMLSignature(sigElement, "");

        // Check we have a KeyInfo
        KeyInfo ki = signature.getKeyInfo();
        Assert.assertNotNull(ki);

        // Check the Signature value
        Assert.assertTrue(signature.checkSignatureValue(cert));
    }




    public static void findElementsToVerify(Document document, List<QName> namesToSign) throws XPathExpressionException {
        XPathFactory xpf = XPathFactory.newInstance();
        XPath xpath = xpf.newXPath();
        xpath.setNamespaceContext(new DSNamespaceContext());

        for (QName nameToSign : namesToSign) {
            String expression = "//*[local-name()='" + nameToSign.getLocalPart() + "']";
            Element signedElement =
                (Element) xpath.evaluate(expression, document, XPathConstants.NODE);
            Assert.assertNotNull(signedElement);
            if (signedElement.hasAttributeNS(null, "Id")) {
                signedElement.setIdAttributeNS(null, "Id", true);
            }
        }
    }

    public static Element getSignatureElement(Document document) throws XPathExpressionException {
        XPathFactory xpf = XPathFactory.newInstance();
        XPath xpath = xpf.newXPath();
        xpath.setNamespaceContext(new DSNamespaceContext());

        // Find the Signature Element
        String expression = "//dsig:Signature[1]";
        Element sigElement =
                (Element) xpath.evaluate(expression, document, XPathConstants.NODE);
        return sigElement;
    }
}

The generated xml file with signatue, It's not well formatted, I would like to format it, but then it caused the verification to fail.

<PurchaseOrder xmlns="urn:example:po">
  <Items>
    <Item Code="001-001-001" Quantity="1">
      spade
    </Item>
    <Item Code="001-001-002" Quantity="1">
      shovel
    </Item>
  </Items>
  <ShippingAddress>
    Dig PLC, 1 First Ave, Dublin 1, Ireland
  </ShippingAddress>
  <PaymentInfo Id="27a5af39-7896-4002-ad65-a4ec9015bf23">
    <BillingAddress>
      Dig PLC, 1 First Ave, Dublin 1, Ireland
    </BillingAddress>
    <CreditCard Type="Amex">
      <Name>Foo B Baz</Name>
      <Number>1234 567890 12345</Number>
      <Expires Month="1" Year="2005"></Expires>
    </CreditCard>
  </PaymentInfo>
 <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
    <ds:SignedInfo><!--If I add a lot of blank spaces at the start of this line, the verification is ok-->
<ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"></ds:CanonicalizationMethod><!--If I add a blank space at the start of this line, the verification is not working-->
<ds:SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"></ds:SignatureMethod>
<ds:Reference URI="#27a5af39-7896-4002-ad65-a4ec9015bf23">
<ds:Transforms>
<ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"></ds:Transform>
</ds:Transforms>
<ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"></ds:DigestMethod>
<ds:DigestValue>ANOpAn94IZWzHAFvQSXI6rK3Hsg=</ds:DigestValue>
</ds:Reference>
</ds:SignedInfo>
<ds:SignatureValue>
Tlbljw+A69WQ4oClxMgv64RLunmxX0k92UJdqGkQUo7L35wpVgET1EjtZf7TF6ekaxuICDEISPmq&#xD;
R7vwrRsmf3/u1v9bANaa0rOtO7nuJgxUs9kVndnMnAPWcIm9Njtl7GUEpt6BLMKwAVkbDjOq5Q0S&#xD;
R3P9d31ppuYdOt7nnbGvgtYDDVIsQRA50rlZqotWI3odfh27HlJMtIys9YJ/0BuQKI4LaNhdymKQ&#xD;
haRFe6jaAv3v0MVG9Hg53uaJf8vwKL0tYG73ZO2Wh2Tr0j0EPEJY72akyJdNcAHjHiSzZEw1HwRt&#xD;
Y9Ns2cnbMLpVEYQzW8oO/TO8uf9UYXfUttFyLQ==
</ds:SignatureValue>
<ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
<ds:X509Data>
<ds:X509Certificate>
MIIDqjCCAxOgAwIBAgIBHzANBgkqhkiG9w0BAQsFADBmMQswCQYDVQQGEwJERTEPMA0GA1UECBMG&#xD;
QmF5ZXJuMQ8wDQYDVQQHEwZNdW5pY2gxDTALBgNVBAoTBEhvbWUxFTATBgNVBAsTDEFwYWNoZSBX&#xD;
U1M0SjEPMA0GA1UEAxMGV2VybmVyMB4XDTE1MDkxMDEzMjEyMloXDTI1MDkwNzEzMjEyMlowUzEL&#xD;
MAkGA1UEBhMCSUUxETAPBgNVBAgTCExlaW5zdGVyMQ8wDQYDVQQHEwZEdWJsaW4xDzANBgNVBAoT&#xD;
BkFwYWNoZTEPMA0GA1UEAxMGQ2xpZW50MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA&#xD;
jeP2hF/KOv8Pw/++n9bAa0DxKFzyLvlcAciBgv1E01cCyUwn5X0v8nT9gIdjXBi6Y6PvSCs2GqcA&#xD;
zo82yzIjlcDmocXt/2vBkezwh6Ow2xoXItzXD+I6KlhpH8gl4xXd35N0z+88m4SEclncm//l3sQH&#xD;
P6CMG9O4H91A+0DXzTkQHzs2PEg13ONMOVbg5P85ceZ+VTBjd3BkqC0mZDB6Ovo6Xt06tVS/S4rF&#xD;
R0L9mtyxZ+v/Psd8TYAIk3wlz1e1wm+dobp1YkOPgvv08r8ROVnWC0pRZquCPQUou6r50LseRZWE&#xD;
o9MGN8kt4sYcLCaJbF3fsTvKYi6InWyczv2JSQIDAQABo4H2MIHzMAkGA1UdEwQCMAAwLAYJYIZI&#xD;
AYb4QgENBB8WHU9wZW5TU0wgR2VuZXJhdGVkIENlcnRpZmljYXRlMB0GA1UdDgQWBBQsRXQjSEq1&#xD;
PetSTIekkXnMBtntGTCBmAYDVR0jBIGQMIGNgBRWF+/2a4tZ/iMZaN54wOFNZ33QZqFqpGgwZjEL&#xD;
MAkGA1UEBhMCREUxDzANBgNVBAgTBkJheWVybjEPMA0GA1UEBxMGTXVuaWNoMQ0wCwYDVQQKEwRI&#xD;
b21lMRUwEwYDVQQLEwxBcGFjaGUgV1NTNEoxDzANBgNVBAMTBldlcm5lcoIJAI3hLAppEXfSMA0G&#xD;
CSqGSIb3DQEBCwUAA4GBAKZXwqAplMzutyC8jIUzFN/DfQOAuZ8ExLBIUxW51KSzxxL+kfydq4HF&#xD;
otRC7PZ3bYEkXYN7deUYoq6yFO0AAWvagBbxdzRYDPRCwpKe+4jn8VLGtjkEyLSsMiiaNiOuMx8q&#xD;
WxvV1sA9KioHvZDFVP+0QqSc1ZytEPOKasdViUU9
</ds:X509Certificate>
</ds:X509Data>
</ds:KeyInfo>
</ds:Signature></PurchaseOrder>

Update 1:

  1. The original xml is well formatted. Currently, only the ds:Signature node is not well formatted.
  2. When process the sign with the apache library, How do we know the XML file is already formatted? Because the XML file is now represented as Java objects.
  3. Usually, the content between > and <, like in this piece of <Item>this is content</Item>, this is content is the payload, Does the signing process only sign the payload? or it will contain the <Item>, <\Item>, even more it will include some blank space before the <Item> like <Item> and some \n in the end of <\Item>?

Upvotes: 0

Views: 2338

Answers (2)

iambenna
iambenna

Reputation: 61

In case you want to remove the Carriage Return (&#xD;) at the end of each line inside the <ds:SignatureValue> and the <ds:X509Certificate> you can try adding (alternatively) these options to the JVM (or Servlet container):

  • -Dcom.sun.org.apache.xml.internal.security.ignoreLineBreaks=true

or

  • -Dorg.apache.xml.security.ignoreLineBreaks=true

Upvotes: 1

Timothy Legge
Timothy Legge

Reputation: 559

A fair number of questions there.

  1. The cert in the keystore validated that the XML is signed by the expected key. You can't implicitly trust the signature in the XML file. It's like me providing you a picture ID that consists of a mirror with "me" written at the top. It might be correct but it's not real reliable.

  2. Formatting XML must be done before it is signed. The canonicalization of XML is meant to ensure that the XML being signed is equivalent. It processes the XML with rules to ensure that equivalent XML would have the same signature but some formatting and tidying after the signature would break it. While I do like nicely formatted XML it's meant to be processed by by computers so don't worry too much about it...

Update 1:

  1. The original xml is well formatted. Currently, only the ds:Signature node is not well formatted.

Not much to be done with that in general. Signing the XML requires the library to create an xml fragment containing a digest for the canonical version of your XML. The fragment is then signed and the result is another XML fragment. In general they are independent in that as long as you don't change what makes an equivalent canonical xml but there are enough rules to make it difficult for the library to make it pretty when combined. You would need to have a combined original XML and Signature in a pretty format and then update the Signature/SignedInfo with the parts that change. For example insert the new signature value for the SignedInfo into a complete document that you know is well formatted.

  1. When process the sign with the apache library, How do we know the XML file is already formatted? Because the XML file is now represented as Java objects.

Most (many? some?) XML libraries have a function that can format the XML as "pretty" (or well formatted). However the nature of Signed XML would mean that the signature could be affected by doing that.

  1. Usually, the content between > and <, like in this piece of this is content, this is content is the payload, Does the signing process only sign the payload? or it will contain the , <\Item>, even more it will include some blank space before the like and some \n in the end of <\Item>?

The rules vary somewhat by canonicalization method. See https://www.w3.org/TR/xml-c14n/#Terminology for example. But I would expect that

All whitespace in character content is retained (excluding characters removed during line feed normalization)

Upvotes: 0

Related Questions