This is an automated email from the ASF dual-hosted git repository.
coheigea pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/ws-wss4j.git
The following commit(s) were added to refs/heads/master by this push:
new 6726da983 WSS-727 Do not cache SwA attachment References when
verifying a Signature (#640)
6726da983 is described below
commit 6726da983f26550c6452e1eccab59e8b08f3ddeb
Author: shunkica <[email protected]>
AuthorDate: Tue Sep 22 13:12:18 2026 +0200
WSS-727 Do not cache SwA attachment References when verifying a Signature
(#640)
* WSS-727 Do not cache SwA attachment References when verifying a Signature
SignatureProcessor sets javax.xml.crypto.dsig.cacheReference=TRUE on the
DOMValidateContext. In Santuario that single property enables two caches in
DOMReference: the dereferenced Data, and the pre-digested input bytes. The
second one makes DigesterOutputStream retain every octet fed to the digest,
so
for a signed SwA attachment the whole attachment is held in an
UnsyncByteArrayOutputStream that grows by doubling - roughly 2.3x the
attachment size in heap, no matter how the attachment is backed. Signing the
same message is streaming. The only consumer of the cached digest input is
Reference.getDigestInputStream(), which WSS4J never calls.
Replace XMLSignature.validate(context) with the equivalent explicit loop -
SignatureValue check plus per-Reference Reference.validate() - and turn
cacheReference off for attachment References only. buildProtectedRefs then
recognises attachment References by their Transform algorithm instead of by
their dereferenced Data, producing the same WSDataRef as before.
Non-attachment References keep cacheReference=TRUE, so element recovery, STR
dereferencing and the checks that depend on them are unaffected.
The debug block that logs per-Reference status after a failed verification
applies the same rule. Reference.validate() caches its result, so References
the loop already validated are only re-logged, but the ones it never reached
are validated there for the first time - and on a SignatureValue mismatch,
the
usual failure, that is all of them. Otherwise enabling debug logging to
diagnose a failing signature would buffer every signed attachment on the
message that just failed.
* WSS-727 Clarify validateSignature and attachment DataRef test Javadoc
validateSignature is not equivalent to XMLSignature.validate(context):
it does not validate ds:Manifest References and does not memoise the
result. The DataRef test derives the attachment WSDataRef from the
Transform algorithm and therefore does not verify non-caching.
---
.../wss4j/dom/processor/SignatureProcessor.java | 57 ++++++++++++++++++++-
.../apache/wss4j/dom/message/AttachmentTest.java | 59 ++++++++++++++++++++++
2 files changed, 115 insertions(+), 1 deletion(-)
diff --git
a/ws-security-dom/src/main/java/org/apache/wss4j/dom/processor/SignatureProcessor.java
b/ws-security-dom/src/main/java/org/apache/wss4j/dom/processor/SignatureProcessor.java
index 8db6097e7..83ed92287 100644
---
a/ws-security-dom/src/main/java/org/apache/wss4j/dom/processor/SignatureProcessor.java
+++
b/ws-security-dom/src/main/java/org/apache/wss4j/dom/processor/SignatureProcessor.java
@@ -44,6 +44,7 @@ import javax.xml.crypto.dsig.SignedInfo;
import javax.xml.crypto.dsig.Transform;
import javax.xml.crypto.dsig.XMLObject;
import javax.xml.crypto.dsig.XMLSignature;
+import javax.xml.crypto.dsig.XMLSignatureException;
import javax.xml.crypto.dsig.XMLSignatureFactory;
import javax.xml.crypto.dsig.XMLValidateContext;
import javax.xml.crypto.dsig.dom.DOMValidateContext;
@@ -402,7 +403,7 @@ public class SignatureProcessor implements Processor {
setElementsOnContext(xmlSignature, (DOMValidateContext)context,
data, wsDocInfo);
- boolean signatureOk = xmlSignature.validate(context);
+ boolean signatureOk = validateSignature(xmlSignature, context);
if (signatureOk) {
// Only now that the signature has actually been validated may
the identifier be
// added to the replay cache. Adding it beforehand lets an
attacker poison the
@@ -426,6 +427,11 @@ public class SignatureProcessor implements Processor {
xmlSignature.getSignedInfo().getReferences().iterator();
while (referenceIterator.hasNext()) {
Reference reference = (Reference)referenceIterator.next();
+ // References that validateSignature did not reach are
validated here for the
+ // first time - all of them when the SignatureValue itself
failed - so keep
+ // attachment caching off for those too
+ context.setProperty("javax.xml.crypto.dsig.cacheReference",
+ !isAttachmentReference(reference));
boolean referenceValidationCheck =
reference.validate(context);
String id = reference.getId();
if (id == null) {
@@ -444,6 +450,48 @@ public class SignatureProcessor implements Processor {
throw new
WSSecurityException(WSSecurityException.ErrorCode.FAILED_CHECK);
}
+ /**
+ * Validates the SignatureValue and then every SignedInfo Reference, as
XMLSignature.validate(context)
+ * does, but with Reference caching turned off for SwA attachment
References. Unlike
+ * XMLSignature.validate(context), ds:Manifest References are not
validated and the result is not
+ * memoised on the XMLSignature.
+ *
+ * Caching a Reference makes Santuario retain every octet fed to the
digest, which for an attachment
+ * is the whole attachment - and the only consumer of that copy is
Reference.getDigestInputStream(),
+ * which WSS4J never calls. The dereferenced Data of an attachment
Reference is not needed either,
+ * as buildProtectedRefs recognises attachment References by their
Transform algorithm.
+ */
+ private boolean validateSignature(XMLSignature xmlSignature,
XMLValidateContext context)
+ throws XMLSignatureException {
+ if (!xmlSignature.getSignatureValue().validate(context)) {
+ return false;
+ }
+ try {
+ for (Object referenceObject :
xmlSignature.getSignedInfo().getReferences()) {
+ Reference reference = (Reference)referenceObject;
+ context.setProperty("javax.xml.crypto.dsig.cacheReference",
+ !isAttachmentReference(reference));
+ if (!reference.validate(context)) {
+ return false;
+ }
+ }
+ return true;
+ } finally {
+ context.setProperty("javax.xml.crypto.dsig.cacheReference",
Boolean.TRUE);
+ }
+ }
+
+ private static boolean isAttachmentReference(Reference reference) {
+ for (Object transformObject : reference.getTransforms()) {
+ String algorithm = ((Transform)transformObject).getAlgorithm();
+ if (WSConstants.SWA_ATTACHMENT_CONTENT_SIG_TRANS.equals(algorithm)
+ ||
WSConstants.SWA_ATTACHMENT_COMPLETE_SIG_TRANS.equals(algorithm)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
/**
* Retrieve the Reference elements and set them on the ValidateContext
* @param xmlSignature the XMLSignature object to get the references from
@@ -563,6 +611,13 @@ public class SignatureProcessor implements Processor {
Element se = dereferenceSTR(doc, siRef, requestData,
wsDocInfo);
// If an STR Transform is not used then just find the cached
element
boolean attachment = false;
+ if (se == null && isAttachmentReference(siRef)) {
+ // Attachment References are not cached, so recognise them
by their
+ // Transform algorithm rather than by their dereferenced
Data
+ se =
doc.createElementNS("http://docs.oasis-open.org/wss/oasis-wss-SwAProfile-1.1",
+ "attachment");
+ attachment = true;
+ }
if (se == null) {
Data dereferencedData = siRef.getDereferencedData();
if (dereferencedData instanceof NodeSetData) {
diff --git
a/ws-security-dom/src/test/java/org/apache/wss4j/dom/message/AttachmentTest.java
b/ws-security-dom/src/test/java/org/apache/wss4j/dom/message/AttachmentTest.java
index 68a0751af..dbb404709 100644
---
a/ws-security-dom/src/test/java/org/apache/wss4j/dom/message/AttachmentTest.java
+++
b/ws-security-dom/src/test/java/org/apache/wss4j/dom/message/AttachmentTest.java
@@ -50,9 +50,11 @@ import org.apache.wss4j.common.util.KeyUtils;
import org.apache.wss4j.common.util.SOAPUtil;
import org.apache.wss4j.common.util.XMLUtils;
import org.apache.wss4j.dom.WSConstants;
+import org.apache.wss4j.dom.WSDataRef;
import org.apache.wss4j.dom.common.KeystoreCallbackHandler;
import org.apache.wss4j.dom.engine.WSSConfig;
import org.apache.wss4j.dom.engine.WSSecurityEngine;
+import org.apache.wss4j.dom.engine.WSSecurityEngineResult;
import org.apache.wss4j.dom.handler.RequestData;
import org.apache.wss4j.dom.handler.WSHandlerResult;
import org.junit.jupiter.api.Test;
@@ -62,6 +64,7 @@ import org.w3c.dom.NodeList;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
import static org.junit.jupiter.api.Assumptions.assumeFalse;
@@ -149,6 +152,62 @@ public class AttachmentTest {
assertEquals("text/xml", responseAttachment.getMimeType());
}
+ /**
+ * The WSDataRef of a signed attachment must be flagged as an attachment
and carry the
+ * synthesised SwA "attachment" element. buildProtectedRefs derives this
from the Reference's
+ * Transform algorithm rather than from the dereferenced Data, so these
assertions hold whether
+ * or not the attachment Reference was cached during validation.
Non-caching itself is not
+ * verified here: its only observable effect is heap usage.
+ */
+ @Test
+ @SuppressWarnings("unchecked")
+ public void testXMLAttachmentContentSignatureDataRef() throws Exception {
+ Document doc = SOAPUtil.toSOAPPart(SOAPUtil.SAMPLE_SOAP_MSG);
+ WSSecHeader secHeader = new WSSecHeader(doc);
+ secHeader.insertSecurityHeader();
+
+ WSSecSignature builder = new WSSecSignature(secHeader);
+ builder.setUserInfo("16c73ab6-b892-458f-abf5-2f875f74882e",
"security");
+
+ builder.getParts().add(new WSEncryptionPart("Body",
"http://schemas.xmlsoap.org/soap/envelope/", "Content"));
+ builder.getParts().add(new WSEncryptionPart("cid:Attachments",
"Content"));
+
+ final String attachmentId = UUID.randomUUID().toString();
+ final Attachment attachment = new Attachment();
+ attachment.setMimeType("text/xml");
+ attachment.addHeaders(getHeaders(attachmentId));
+ attachment.setId(attachmentId);
+ attachment.setSourceStream(new
ByteArrayInputStream(SOAPUtil.SAMPLE_SOAP_MSG.getBytes(StandardCharsets.UTF_8)));
+
+ builder.setAttachmentCallbackHandler(
+ new
AttachmentCallbackHandler(Collections.singletonList(attachment)));
+ Document signedDoc = builder.build(crypto);
+
+ WSHandlerResult results =
+ verify(signedDoc, new
AttachmentCallbackHandler(Collections.singletonList(attachment)));
+
+ WSSecurityEngineResult actionResult =
+ results.getActionResults().get(WSConstants.SIGN).get(0);
+ List<WSDataRef> refs =
+
(List<WSDataRef>)actionResult.get(WSSecurityEngineResult.TAG_DATA_REF_URIS);
+ assertEquals(2, refs.size());
+
+ WSDataRef attachmentRef = null;
+ for (WSDataRef ref : refs) {
+ if (ref.isAttachment()) {
+ attachmentRef = ref;
+ }
+ }
+ assertNotNull(attachmentRef);
+ assertEquals("cid:" + attachmentId, attachmentRef.getWsuId());
+ Element protectedElement = attachmentRef.getProtectedElement();
+ assertEquals("attachment", protectedElement.getLocalName());
+ assertEquals("http://docs.oasis-open.org/wss/oasis-wss-SwAProfile-1.1",
+ protectedElement.getNamespaceURI());
+ assertTrue(attachmentRef.getTransformAlgorithms()
+ .contains(WSConstants.SWA_ATTACHMENT_CONTENT_SIG_TRANS));
+ }
+
@Test
public void testInvalidXMLAttachmentContentSignature() throws Exception {
Document doc = SOAPUtil.toSOAPPart(SOAPUtil.SAMPLE_SOAP_MSG);