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);

Reply via email to