WSS-727 Do not cache SwA attachment References when verifying a Signature - #640
Open
shunkica wants to merge 1 commit into
Open
WSS-727 Do not cache SwA attachment References when verifying a Signature#640shunkica wants to merge 1 commit into
shunkica wants to merge 1 commit into
Conversation
…ture 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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
JIRA: https://issues.apache.org/jira/browse/WSS-727
The Problem
When you verify a SOAP message with a signed SwA attachment, it needs about 2.3x the attachment's size in heap memory. This happens even if the attachment is already on disk or if you provide a streaming source. Compare that to signing the same message, which only needs ~20 MB consistently. That's a huge difference.
The culprit is a single flag in
SignatureProcessor.verifyXMLSignature():This flag tells Santuario to cache stuff, but it's a blunt instrument. In
DOMReference.transform()(xmlsec 4.0.4), it flips on two separate caches that have nothing to do with each other:The two caches:
buildProtectedRefs()to figure out what each Reference covered.DigesterOutputStreambuffers it all in anUnsyncByteArrayOutputStream, which doubles in size as it grows, then copies the whole thing again ingetInputStream().For attachments,
AttachmentContentSignatureTransformfeeds the attachment directly into that stream. So a 200 MB attachment ends up consuming a 256 MB backing array plus a 200 MB copy - ~456 MB live at once. This buffered copy is useless: the only thing that reads it isReference.getDigestInputStream(), and WSS4J never calls that.The Fix
The solution has two parts:
Replace
xmlSignature.validate(context)with an explicit loop. Instead of one call, we do:SignatureValueReferenceindividuallycacheReferenceUpdate
buildProtectedRefs()to recognize attachments by their Transform algorithm. This is essential. With caching off,getDereferencedData()returnsnull, so toggling the flag alone would break every signed attachment withFAILED_CHECK. The fix keepsbuildProtectedRefs()working: it still produces the sameWSDataRefwith the synthesised<attachment>element andsetAttachment(true).Why this preserves the existing behavior:
The flag is read per-Reference at transform time, so we can toggle it between
Reference.validate()calls without issues.Reference.validate()caches its result, so when debug logging re-validates a Reference we already checked, it just uses the cached status, it doesn't re-transform. References the loop hasn't reached yet get validated there, and the caching rule applies to them too. This matters more than it sounds: if the SignatureValue check fails (a common failure), we bail out before validating any References. Without this, turning on debug logging to diagnose the failure would buffer every signed attachment on that message, leading to the very OutOfMemoryError we're fixing. The old code did exactly this.Non-attachment References still cache, so element recovery, the STR-dereference path (WSS-222), and the anti-wrapping checks all keep working. We restore the flag to
TRUEwhen we're done.The explicit loop is fully equivalent to
DOMXMLSignature.validate(): Manifest validation only runs iforg.jcp.xml.dsig.validateManifestsis set (WSS4J never sets it), and the XMLSignature's cached validation status is never read because the object doesn't escapeSignatureProcessor.The short-circuit semantics match the old behavior - still bail on first failure.
ws-security-staxdoesn't use this property, so no impact there.Testing
A new test,
AttachmentTest.testXMLAttachmentContentSignatureDataRef, verifies that theWSDataReffor a signed attachment still hasisAttachment()set and still carries the synthesised SwAattachmentelement. This ensures thebuildProtectedRefs()change does what it's supposed to.Memory measurements
200 MB attachment from the WSS-727 repro; source stream is mark/reset-capable so it contributes no heap
Notes
History: The
cacheReferenceflag has been there since the WSS4J 2.0 rename (commit f647a91), so the bug affects 2.x and 3.x as well. Tested on 4.0.1 and current master.The bigger picture: The digest input buffer also costs ~3.2x the signed content for regular element References (minimum -Xmx: 160m vs 96m for a 20 MB Body, or 288m vs 160m for 40 MB). The key difference is asymptotic: element content is already in the DOM, so the cache is a constant-factor overhead you can outrun by raising heap; attachment content is streaming and otherwise never in memory, so the cache turns constant-heap streaming into O(attachment-size) that no fixed heap covers. This fix is narrowly scoped to SwA attachments - the only case with no caller-side workaround.
For element and STR References, the dereferenced data is the authoritative record of what was digested and is essential for
WSDataRefand the anti-wrapping checks. You can't just disable caching for those without losing that data. A proper fix for all Reference types would require Santuario to expose separate properties for the two caches instead of one toggle for both - that would letbuildProtectedRefs()fetch the dereferenced data without buffering the digest input. That's a design change upstream that would need a new Santuario release.Related: WSS-638 is about
processAttachment()buffering non-mark-capable source streams viaBufferedInputStream.mark(Integer.MAX_VALUE). That one still happens on both sign and verify, and it's not fixed here. The workaround is to supply a disk-backed, mark-capable stream. The bug we're fixing had no workaround because the digest-side cache retained the attachment regardless of how the stream behaved.