XML 向文档显式添加元素时签名未验证 (JAVA)?

XML Signature not validating when adding an element explicitly to the document (JAVA)?

我正在创建以下 XML 文档,使用第三方提供的给定 XSD 的 JAXB。第三方请求签署文档并向其添加一个额外的元素来保存签名。使用 JDK 1.7.

下面是编组的代码示例:

JAXBContext jaxbContext = JAXBContext.newInstance(DataPDU.class);
DataPDU myDataPDU = new DataPDU();
myDataPDU.setRevision("2.0.6");

// marshall the file
Marshaller marshaller = jaxbContext.createMarshaller();

DOMResult domResult = new DOMResult();
marshaller.marshal(myDataPDU, domResult);

// get the document list
Document document = (Document) domResult.getNode();

然后我如下创建元素 (LAU) 并使用 HMAC-SHA256 算法和 JSR105 JAVA API 对文档进行签名(我不会包含整个签名代码为了减少冗长,我使用 XMLSignature class 的标准行为,然后使用 XML 转换器将文档转换为来自 DOMSource 的文件输出流:

Element LAUElement = document.createElementNS("urn:swift:saa:xsd:saa.2.0", "Saa:LAU");
Element rootElement = document.getDocumentElement();
rootElement.appendChild(LAUElement);

// sign the document
XMLSignatureUtil.sign(document, secret, LAUElement, "ds");
// create the output file
TransformerUtil.transformDocumentToFile(document, "resultingFile.xml");

XML 签名正确,但在验证时,计算出的摘要值与摘要值不同。

我注意到在创建 LAU 元素时更改命名空间值时,摘要永远不会改变,就好像文档正在签名并忽略了 LAU 元素的命名空间,我想这就是为什么它的失败。整个文档中的任何其他更改,或 LAU 元素前缀的更改都会直接影响有效负载的计算摘要。

如果我直接将签名附加到根元素而不是创建 LAU 元素,验证工作正常。

LAU 元素存在于 XSD 中,可以使用 JAXB 创建,但问题是我无法找到一种方法来分配与根元素。

问题:

Is the namespace actually being omitted from the payload digest calculation when adding the element to the document using createElementNS and appendChild?

Is there a way to provide through JAXB a prefix for the same root namespace to a single element only?

How can I find the actual XML string being signed by the API, I tried reading the reference input stream after enabling javax.xml.crypto.dsig.cacheReference but this did not work when signing, it only worked when validating?

下面是 XML 的示例:

<DataPDU xmlns="urn:swift:saa:xsd:saa.2.0">
  <Revision>2.0.6</Revision>
  <Saa:LAU xmlns:Saa="urn:swift:saa:xsd:saa.2.0"> Signature lies here </Saa:LAU>
</DataPDU>

更新 - 完整 XML 签名过程

JAXBContext jaxbContext = JAXBContext.newInstance(DataPDU.class);
DataPDU myDataPDU = new DataPDU();
myDataPDU.setRevision("2.0.6");

// marshall the file
Marshaller marshaller = jaxbContext.createMarshaller();

DOMResult domResult = new DOMResult();
marshaller.marshal(myDataPDU, domResult);

// get the document list
Document document = (Document) domResult.getNode();

// signing process
XMLSignatureFactory factory = XMLSignatureFactory.getInstance("DOM");

SignatureMethod signatureMethod =
    factory.newSignatureMethod("http://www.w3.org/2001/04/xmldsig-more#hmac-sha256", null);

CanonicalizationMethod canonicalizationMethod =
    factory.newCanonicalizationMethod(CanonicalizationMethod.EXCLUSIVE, (XMLStructure) null);

List<Transform> transforms = new ArrayList<Transform>();
transforms.add(factory.newTransform(Transform.ENVELOPED, (XMLStructure) null));
transforms.add(factory.newTransform("http://www.w3.org/2001/10/xml-exc-c14n#", (XMLStructure) null));

DigestMethod digestMethod = factory.newDigestMethod("http://www.w3.org/2001/04/xmlenc#sha256", null);

Reference reference = factory.newReference("", digestMethod, transforms, null, null);

SignedInfo signedInfo =
    factory.newSignedInfo(canonicalizationMethod, signatureMethod, Collections.singletonList(reference));

String secretKey = "Abcd1234abcd1234Abcd1234abcd1234";
SecretKeySpec secret_key = new SecretKeySpec(secretKey.getBytes(), "HmacSHA256");

Element LAUElement = document.createElementNS("urn:swift:saa:xsd:saa.2.0", "Saa:LAU");
Element rootElement = document.getDocumentElement();
rootElement.appendChild(LAUElement);

DOMSignContext domSignContext = new DOMSignContext(secret_key, LAUElement);
domSignContext.setDefaultNamespacePrefix("ds");

XMLSignature signature = factory.newXMLSignature(signedInfo, null);
signature.sign(domSignContext);

How can I find the actual XML string being signed by the API, I tried reading the reference input stream after enabling javax.xml.crypto.dsig.cacheReference but this did not work when signing, it only worked when validating?

幸运的是,默认实现(假设正在使用)提供了一些调试功能。这是使用 FINEST 粒度登录到控制台的设置示例,尽管 FINE 似乎就足够了。

handlers= java.util.logging.ConsoleHandler
java.util.logging.ConsoleHandler.level = FINER
java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter
org.jcp.xml.dsig.internal.level = FINER
com.sun.org.apache.xml.internal.security.level = FINER

您可以将其另存为 logging.properties 文件并通过命令行选项 -Djava.util.logging.config.file 提供其路径,但也可以通过编程方式完成。

这样做实际上会显示已签名的规范化 XML,这回答了这个问题:

Is the namespace actually being omitted from the payload digest calculation when adding the element to the document using createElementNS and appendChild?

确实如此。 运行 我在日志记录中看到的代码是用于签名的规范化 XML。

<DataPDU xmlns="urn:swift:saa:xsd:saa.2.0"><Revision>2.0.6</Revision><Saa:LAU></Saa:LAU></DataPDU>

注意 Saa 前缀是如何存在的,但名称空间声明却不存在。然而,信息在那里,因为转换后带有前缀的声明显示在结果中。这就解释了为什么更改命名空间 URI 本身不会导致不同的摘要,但更改前缀会导致不同的摘要。不知道为什么会这样。感觉不应该,但是规范化版本中包含的内容的规则在规范中非常混乱,以至于我遗漏了一些东西,或者它是实现中的错误。解决方法是在创建 LAUElement:

后添加此行
 LAUElement.setAttribute("xmlns:Saa", "urn:swift:saa:xsd:saa.2.0");

我认为 DOMResult 可能使用了一个未设置为命名空间感知的文档生成器,但我试过这个没有任何区别:

DocumentBuilderFactory domFactory = DocumentBuilderFactory.newInstance();
domFactory.setNamespaceAware(true);
DocumentBuilder docBuilder = domFactory.newDocumentBuilder();
Document doc = docBuilder.newDocument();
DOMResult domResult = new DOMResult(doc);
marshaller.marshal(myDataPDU, domResult);

可能是DOMResult本身的bug。解决方法有点混乱,但它完成了工作。这实际上改变了摘要。我已经通过提供 LAUElement 和文档根目录作为 DOMSignContext 的输入来尝试这样做,这在这两种情况下都会产生相同的签名(摘要和签名值),尽管在后一种情况下签名添加到根,而不是元素。

这也告诉我们整个文档都用作签名的输入,即使您提供元素并使用独有的规范化方法也是如此。它只会更改放置签名的位置。 URI 解析找到根元素及其下方的所有元素的祖先。这让我有点吃惊。

通过上述操作,我成功地使用以下代码正确验证了签名:

XMLSignatureFactory factory = XMLSignatureFactory.getInstance("DOM");

NodeList sigList = doc.getElementsByTagNameNS(XMLSignature.XMLNS, "Signature");
if (sigList.getLength() == 0) {
    throw new Exception("Cannot find Signature element");
}
String secretKey = "Abcd1234abcd1234Abcd1234abcd1234";
SecretKeySpec secret_key = new SecretKeySpec(secretKey.getBytes(), "HmacSHA256");
DOMValidateContext valContext = new DOMValidateContext(secret_key, sigList.item(0));
XMLSignature signature = factory.unmarshalXMLSignature(valContext);

System.out.println("Core validity: " + signature.validate(valContext));

问题确实似乎是当命名空间声明被删除时,生成的摘要与验证时生成的摘要不同。

Is there a way to provide through JAXB a prefix for the same root namespace to a single element only?

简短的回答是,没有简单的方法可以做到这一点。完整的答案可以在这里找到:

编辑:

补充说明。小心将已签名文档输出到文件的转换器。如果将其设置为缩进输出,则生成的文件实际上将具有不同的摘要。可以忽略空格(如 <empty /><empty/>),但在元素中它通常被视为文本节点和计算摘要的规范化版本的一部分。