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

2022-09-04 04:43:25

我正在使用第三方提供的给定 XSD 的 JAXB 创建以下 XML 文档。第三方请求对文档进行签名,并向其添加一个保存签名的额外元素。使用 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),如下所示,并使用算法和JSR105 JAVA API对文档进行签名(我不打算包含整个签名代码以减少冗长,我使用类的标准行为,然后使用XML转换器将文档转换为DOMSource的文件输出流):HMAC-SHA256XMLSignature

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 创建,但问题是我找不到一种方法来分配与根元素相同的命名空间的前缀(仅在文档中为它)。

问题:

使用 和 将元素添加到文档时,是否确实从有效负载摘要计算中省略了命名空间?createElementNSappendChild

有没有办法通过 JAXB 为同一根命名空间仅向单个元素提供前缀?

如何找到由API签名的实际XML字符串,我在启用后尝试读取引用输入流,但这在签名时不起作用,仅在验证时起作用?javax.xml.crypto.dsig.cacheReference

下面是 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);

答案 1

如何找到由API签名的实际XML字符串,我在启用javax..xml crypt.dsig.cacheReference后尝试读取引用输入流,但这在签名时不起作用,仅在验证时起作用?

幸运的是,假设正在使用默认实现,则它提供了一些调试功能。下面是使用粒度登录到控制台的设置示例,尽管似乎已足够了。FINESTFINE

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,这回答了以下问题:

使用 createElementNS 和 appendChild 将元素添加到文档时,是否确实从有效负载摘要计算中省略了命名空间?

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

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

请注意前缀存在,但命名空间声明不存在。然而,信息就在那里,因为在转换后,带有前缀的声明会显示在结果中。这就解释了为什么对命名空间 URI 本身的更改不会导致不同的摘要,但更改前缀会导致不同的摘要。不知道为什么会发生这种情况。感觉不应该,但是规范化版本中包含的规则在规范中是如此混乱,以至于我错过了一些东西,或者它是实现中的一个错误。解决此问题的方法是在创建以下行后添加此行:SaaLAUElement

 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本身的一个错误。解决方法有点混乱,但它可以完成工作。这实际上确实改变了摘要。我尝试了这样做,方法是同时提供 和 文档根作为 的输入,在这两种情况下,这会导致相同的签名(摘要和签名值),尽管在后一种情况下,签名被添加到根而不是元素。LAUElementDOMSignContext

这也告诉我们,即使您提供元素并使用独占规范化方法,也会将整个文档用作签名的输入。它仅更改签名的放置位置。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));

看起来问题确实在于,当命名空间声明被删除时,生成的摘要与验证时生成的摘要不同。

有没有办法通过 JAXB 为同一根命名空间仅向单个元素提供前缀?

简短的回答是,没有微不足道的方法可以做到这一点。长答案可以在这里找到:https://stackoverflow.com/a/42301479/630136

编辑:

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


答案 2

推荐