From e3473b0bce2a50a924ab4cdac042635c8c306512 Mon Sep 17 00:00:00 2001 From: Ihor Kuzmanenko Date: Mon, 11 Aug 2025 16:55:42 +0300 Subject: [PATCH 1/6] Added properties for base64 formatting --- .../xml/security/signature/XMLSignature.java | 6 +- .../AbstractEncryptOutputProcessor.java | 15 +- .../xml/security/utils/ElementProxy.java | 4 +- .../apache/xml/security/utils/XMLUtils.java | 141 ++++++++- .../xml/security/utils/XMLUtilsTest.java | 270 ++++++++++++++++++ 5 files changed, 408 insertions(+), 28 deletions(-) create mode 100644 src/test/java/org/apache/xml/security/utils/XMLUtilsTest.java diff --git a/src/main/java/org/apache/xml/security/signature/XMLSignature.java b/src/main/java/org/apache/xml/security/signature/XMLSignature.java index b2ec541e5..658bcaf37 100644 --- a/src/main/java/org/apache/xml/security/signature/XMLSignature.java +++ b/src/main/java/org/apache/xml/security/signature/XMLSignature.java @@ -684,11 +684,7 @@ private void setSignatureValueElement(byte[] bytes) { signatureValueElement.removeChild(signatureValueElement.getFirstChild()); } - String base64codedValue = XMLUtils.encodeToString(bytes); - - if (base64codedValue.length() > 76 && !XMLUtils.ignoreLineBreaks()) { - base64codedValue = "\n" + base64codedValue + "\n"; - } + String base64codedValue = XMLUtils.encodeElementValue(bytes); Text t = createText(base64codedValue); signatureValueElement.appendChild(t); diff --git a/src/main/java/org/apache/xml/security/stax/impl/processor/output/AbstractEncryptOutputProcessor.java b/src/main/java/org/apache/xml/security/stax/impl/processor/output/AbstractEncryptOutputProcessor.java index efa2fa5a8..7e5e5e293 100644 --- a/src/main/java/org/apache/xml/security/stax/impl/processor/output/AbstractEncryptOutputProcessor.java +++ b/src/main/java/org/apache/xml/security/stax/impl/processor/output/AbstractEncryptOutputProcessor.java @@ -24,12 +24,7 @@ import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import java.security.spec.AlgorithmParameterSpec; -import java.util.ArrayDeque; -import java.util.ArrayList; -import java.util.Deque; -import java.util.Iterator; -import java.util.List; -import java.util.Map; +import java.util.*; import java.util.Map.Entry; import javax.crypto.Cipher; @@ -40,7 +35,6 @@ import javax.xml.stream.XMLStreamConstants; import javax.xml.stream.XMLStreamException; -import org.apache.commons.codec.binary.Base64OutputStream; import org.apache.xml.security.algorithms.JCEMapper; import org.apache.xml.security.encryption.XMLCipherUtil; import org.apache.xml.security.exceptions.XMLSecurityException; @@ -175,12 +169,7 @@ public void init(OutputProcessorChain outputProcessorChain) throws XMLSecurityEx symmetricCipher.init(Cipher.ENCRYPT_MODE, encryptionPartDef.getSymmetricKey(), parameterSpec); characterEventGeneratorOutputStream = new CharacterEventGeneratorOutputStream(); - Base64OutputStream base64EncoderStream = null; //NOPMD - if (XMLUtils.isIgnoreLineBreaks()) { - base64EncoderStream = new Base64OutputStream(characterEventGeneratorOutputStream, true, 0, null); - } else { - base64EncoderStream = new Base64OutputStream(characterEventGeneratorOutputStream, true); - } + OutputStream base64EncoderStream = XMLUtils.encodeStream(characterEventGeneratorOutputStream); //NOPMD base64EncoderStream.write(iv); OutputStream outputStream = new CipherOutputStream(base64EncoderStream, symmetricCipher); //NOPMD diff --git a/src/main/java/org/apache/xml/security/utils/ElementProxy.java b/src/main/java/org/apache/xml/security/utils/ElementProxy.java index 7e7828f2f..298fbbe01 100644 --- a/src/main/java/org/apache/xml/security/utils/ElementProxy.java +++ b/src/main/java/org/apache/xml/security/utils/ElementProxy.java @@ -313,9 +313,7 @@ public void addTextElement(String text, String localname) { */ public void addBase64Text(byte[] bytes) { if (bytes != null) { - Text t = XMLUtils.ignoreLineBreaks() - ? createText(XMLUtils.encodeToString(bytes)) - : createText("\n" + XMLUtils.encodeToString(bytes) + "\n"); + Text t = createText(XMLUtils.encodeElementValue(bytes)); appendSelf(t); } } diff --git a/src/main/java/org/apache/xml/security/utils/XMLUtils.java b/src/main/java/org/apache/xml/security/utils/XMLUtils.java index 9027469cd..18170ea19 100644 --- a/src/main/java/org/apache/xml/security/utils/XMLUtils.java +++ b/src/main/java/org/apache/xml/security/utils/XMLUtils.java @@ -56,14 +56,63 @@ /** * DOM and XML accessibility and comfort functions. * + * @implNote + * Following system properties affect XML formatting: + * */ public final class XMLUtils { + private static final Logger LOG = System.getLogger(XMLUtils.class.getName()); + + private static final String IGNORE_LINE_BREAKS_PROP = "org.apache.xml.security.ignoreLineBreaks"; + private static final String BASE64_IGNORE_LINE_BREAKS_PROP = "org.apache.xml.security.base64.ignoreLineBreaks"; + private static final String BASE64_LINE_SEPARATOR_PROP = "org.apache.xml.security.base64.lineSeparator"; + private static final String BASE64_LINE_LENGTH_PROP = "org.apache.xml.security.base64.lineLength"; + private static boolean ignoreLineBreaks = AccessController.doPrivileged( - (PrivilegedAction) () -> Boolean.getBoolean("org.apache.xml.security.ignoreLineBreaks")); + (PrivilegedAction) () -> Boolean.getBoolean(IGNORE_LINE_BREAKS_PROP)); + + private static Base64FormattingOptions base64Formatting = + AccessController.doPrivileged((PrivilegedAction) () -> { + Base64FormattingOptions options = new Base64FormattingOptions(); + options.setIgnoreLineBreaks(Boolean.getBoolean(BASE64_IGNORE_LINE_BREAKS_PROP)); + + String lineSeparator = System.getProperty(BASE64_LINE_SEPARATOR_PROP); + if (lineSeparator != null) { + try { + options.setLineSeparator(Base64LineSeparator.valueOf(lineSeparator.toUpperCase())); + } catch (IllegalArgumentException e) { + LOG.log(Level.WARNING, "Illegal value of {0} property ignored: {1}", + BASE64_LINE_SEPARATOR_PROP, lineSeparator); + } + } - private static final Logger LOG = System.getLogger(XMLUtils.class.getName()); + Integer lineLength = Integer.getInteger(BASE64_LINE_LENGTH_PROP); + if (lineLength != null && lineLength >= 4) { + options.setLineLength(lineLength); + } else if (lineLength != null) { + LOG.log(Level.WARNING, "Illegal value of {0} property ignored: {1}", + BASE64_LINE_LENGTH_PROP, lineLength); + } + + return options; + }); + + private static Base64.Encoder base64Encoder = (ignoreLineBreaks || base64Formatting.isIgnoreLineBreaks()) ? + Base64.getEncoder() : + Base64.getMimeEncoder(base64Formatting.getLineLength(), base64Formatting.getLineSeparator().getBytes()); + + private static Base64.Decoder base64Decoder = Base64.getMimeDecoder(); private static XMLParser xmlParserImpl = AccessController.doPrivileged( @@ -515,18 +564,48 @@ public static void addReturnBeforeChild(Element e, Node child) { } public static String encodeToString(byte[] bytes) { - if (ignoreLineBreaks) { - return Base64.getEncoder().encodeToString(bytes); + return base64Encoder.encodeToString(bytes); + } + + /** + * Encodes bytes using Base64, with or without line breaks, depending on configuration (see {@link XMLUtils}). + * @param bytes Bytes to encode + * @return Base64 string + */ + public static String encodeElementValue(byte[] bytes) { + String encoded = encodeToString(bytes); + if (!ignoreLineBreaks && !base64Formatting.isIgnoreLineBreaks() + && encoded.length() > base64Formatting.getLineLength()) { + encoded = "\n" + encoded + "\n"; } - return Base64.getMimeEncoder().encodeToString(bytes); + return encoded; + } + + /** + * Wraps output stream for Base64 encoding. + * Output data may contain line breaks or not, depending on configuration (see {@link XMLUtils}) + * @param stream The underlying output stream to write Base64-encoded data + * @return Stream which writes binary data using Base64 encoder + */ + public static OutputStream encodeStream(OutputStream stream) { + return base64Encoder.wrap(stream); } public static byte[] decode(String encodedString) { - return Base64.getMimeDecoder().decode(encodedString); + return base64Decoder.decode(encodedString); } public static byte[] decode(byte[] encodedBytes) { - return Base64.getMimeDecoder().decode(encodedBytes); + return base64Decoder.decode(encodedBytes); + } + + /** + * Wraps input stream for Base64 decoding. + * @param stream Input stream with Base64-encoded data + * @return Input stream with decoded binary data + */ + public static InputStream decodeStream(InputStream stream) { + return base64Decoder.wrap(stream); } public static boolean isIgnoreLineBreaks() { @@ -1068,4 +1147,52 @@ public static byte[] getBytes(BigInteger big, int bitlen) { return resizedBytes; } + + /** + * Aggregates formatting options for base64Binary values. + */ + static class Base64FormattingOptions { + private boolean ignoreLineBreaks = false; + private Base64LineSeparator lineSeparator = Base64LineSeparator.CRLF; + private int lineLength = 76; + + public boolean isIgnoreLineBreaks() { + return ignoreLineBreaks; + } + + public void setIgnoreLineBreaks(boolean ignoreLineBreaks) { + this.ignoreLineBreaks = ignoreLineBreaks; + } + + public Base64LineSeparator getLineSeparator() { + return lineSeparator; + } + + public void setLineSeparator(Base64LineSeparator lineSeparator) { + this.lineSeparator = lineSeparator; + } + + public int getLineLength() { + return lineLength; + } + + public void setLineLength(int lineLength) { + this.lineLength = lineLength; + } + } + + enum Base64LineSeparator { + CRLF(new byte[]{'\r', '\n'}), + LF(new byte[]{'\n'}); + + private byte[] bytes; + + Base64LineSeparator(byte[] bytes) { + this.bytes = bytes; + } + + public byte[] getBytes() { + return bytes; + } + } } diff --git a/src/test/java/org/apache/xml/security/utils/XMLUtilsTest.java b/src/test/java/org/apache/xml/security/utils/XMLUtilsTest.java new file mode 100644 index 000000000..b401291e9 --- /dev/null +++ b/src/test/java/org/apache/xml/security/utils/XMLUtilsTest.java @@ -0,0 +1,270 @@ +package org.apache.xml.security.utils; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; +import java.util.*; +import java.util.stream.Collectors; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.*; +import static org.junit.jupiter.api.Assertions.*; + +public class XMLUtilsTest { + private static final byte[] data = new byte[60]; // long enough for a line break in MIME encoding + + private Properties backup; + private ClassLoader classLoader; + + @BeforeEach + public void createClassLoader() { + /* create custom classloader to reload class in each test */ + ClassLoader parent = getClass().getClassLoader(); + Collection> classesToReload = List.of( + XMLUtils.class, + XMLUtils.Base64FormattingOptions.class, + XMLUtils.Base64LineSeparator.class + ); + classLoader = new ReloadingClassLoader(parent, classesToReload); + ModuleLayer.boot().findModule("org.apache.santuario.xmlsec").orElseThrow() + .addOpens("org.apache.xml.security.parser", classLoader.getUnnamedModule()); + } + + @BeforeEach + public void backupProperties() { + backup = new Properties(); + backup.putAll(System.getProperties()); + } + + @AfterEach + public void restoreProperties() { + System.setProperties(backup); + } + + @Test + public void testAllPropertiesUnset() throws ReflectiveOperationException, IOException { + System.clearProperty("org.apache.xml.security.ignoreLineBreaks"); + System.clearProperty("org.apache.xml.security.base64.ignoreLineBreaks"); + System.clearProperty("org.apache.xml.security.base64.lineSeparator"); + System.clearProperty("org.apache.xml.security.base64.lineLength"); + + Class xmlUtilsClass = classLoader.loadClass(XMLUtils.class.getName()); + String encoded = encodeToString(xmlUtilsClass, data); + String elementValue = encodeElementValue(xmlUtilsClass, data); + String encodedWithStream = encodeUsingStream(xmlUtilsClass, data); + + assertThat(encoded, containsString("\r\n")); + OptionalInt maxLineLength = Arrays.stream(encoded.split("\r\n")).mapToInt(String::length).max(); + assertTrue(maxLineLength.isPresent()); + assertEquals(76, maxLineLength.getAsInt()); + + assertThat(elementValue, containsString(encoded)); + assertThat(elementValue, startsWith("\n")); + assertThat(elementValue, endsWith("\n")); + + assertEquals(encoded, encodedWithStream); + } + + @Test + public void testIgnoreLineBreaksSet() throws ReflectiveOperationException, IOException { + System.setProperty("org.apache.xml.security.ignoreLineBreaks", "true"); + System.clearProperty("org.apache.xml.security.base64.ignoreLineBreaks"); + System.clearProperty("org.apache.xml.security.base64.lineSeparator"); + System.clearProperty("org.apache.xml.security.base64.lineLength"); + + Class xmlUtilsClass = classLoader.loadClass(XMLUtils.class.getName()); + String encoded = encodeToString(xmlUtilsClass, data); + String elementValue = encodeElementValue(xmlUtilsClass, data); + String encodedWithStream = encodeUsingStream(xmlUtilsClass, data); + + assertThat(encoded, not(containsString("\r\n"))); + assertThat(encoded, not(containsString("\n"))); + assertThat(elementValue, not(containsString("\r\n"))); + assertThat(elementValue, not(containsString("\n"))); + + assertEquals(encoded, encodedWithStream); + } + + @Test + public void testIgnoreLineBreaksTakesPrecedence() throws ReflectiveOperationException, IOException { + System.setProperty("org.apache.xml.security.ignoreLineBreaks", "true"); + System.setProperty("org.apache.xml.security.base64.ignoreLineBreaks", "false"); + System.setProperty("org.apache.xml.security.base64.lineSeparator", "crlf"); + System.setProperty("org.apache.xml.security.base64.lineLength", "40"); + + Class xmlUtilsClass = classLoader.loadClass(XMLUtils.class.getName()); + String encoded = encodeToString(xmlUtilsClass, data); + String elementValue = encodeElementValue(xmlUtilsClass, data); + String encodedWithStream = encodeUsingStream(xmlUtilsClass, data); + + assertThat(encoded, not(containsString("\r\n"))); + assertThat(encoded, not(containsString("\n"))); + assertThat(elementValue, not(containsString("\r\n"))); + assertThat(elementValue, not(containsString("\n"))); + + assertEquals(encoded, encodedWithStream); + } + + @Test + public void testBase64IgnoreLineBreaksSet() throws ReflectiveOperationException, IOException { + System.clearProperty("org.apache.xml.security.ignoreLineBreaks"); + System.setProperty("org.apache.xml.security.base64.ignoreLineBreaks", "true"); + System.clearProperty("org.apache.xml.security.base64.lineSeparator"); + System.clearProperty("org.apache.xml.security.base64.lineLength"); + + Class xmlUtilsClass = classLoader.loadClass(XMLUtils.class.getName()); + String encoded = encodeToString(xmlUtilsClass, data); + String elementValue = encodeElementValue(xmlUtilsClass, data); + String encodedWithStream = encodeUsingStream(xmlUtilsClass, data); + + assertThat(encoded, not(containsString("\r\n"))); + assertThat(encoded, not(containsString("\n"))); + assertThat(elementValue, not(containsString("\r\n"))); + assertThat(elementValue, not(containsString("\n"))); + + assertEquals(encoded, encodedWithStream); + } + + @Test + public void testBase64IgnoreLineBreaksTakesPrecedence() throws ReflectiveOperationException, IOException { + System.clearProperty("org.apache.xml.security.ignoreLineBreaks"); + System.setProperty("org.apache.xml.security.base64.ignoreLineBreaks", "true"); + System.setProperty("org.apache.xml.security.base64.lineSeparator", "crlf"); + System.setProperty("org.apache.xml.security.base64.lineLength", "40"); + + Class xmlUtilsClass = classLoader.loadClass(XMLUtils.class.getName()); + String encoded = encodeToString(xmlUtilsClass, data); + String elementValue = encodeElementValue(xmlUtilsClass, data); + String encodedWithStream = encodeUsingStream(xmlUtilsClass, data); + + assertThat(encoded, not(containsString("\r\n"))); + assertThat(encoded, not(containsString("\n"))); + assertThat(elementValue, not(containsString("\r\n"))); + assertThat(elementValue, not(containsString("\n"))); + + assertEquals(encoded, encodedWithStream); + } + + @Test + public void testBase64CustomFormatting() throws ReflectiveOperationException, IOException { + System.clearProperty("org.apache.xml.security.ignoreLineBreaks"); + System.clearProperty("org.apache.xml.security.base64.ignoreLineBreaks"); + System.setProperty("org.apache.xml.security.base64.lineSeparator", "lf"); + System.setProperty("org.apache.xml.security.base64.lineLength", "40"); + + Class xmlUtilsClass = classLoader.loadClass(XMLUtils.class.getName()); + String encoded = encodeToString(xmlUtilsClass, data); + String elementValue = encodeElementValue(xmlUtilsClass, data); + String encodedWithStream = encodeUsingStream(xmlUtilsClass, data); + + assertThat(encoded, not(containsString("\r\n"))); + assertThat(encoded, containsString("\n")); + OptionalInt maxLineLength = Arrays.stream(encoded.split("\n")).mapToInt(String::length).max(); + assertTrue(maxLineLength.isPresent()); + assertEquals(40, maxLineLength.getAsInt()); + + assertThat(elementValue, containsString(encoded)); + assertThat(elementValue, startsWith("\n")); + assertThat(elementValue, endsWith("\n")); + + assertEquals(encoded, encodedWithStream); + } + + @Test + public void testIllegalPropertiesAreIgnored() throws ReflectiveOperationException, IOException { + System.setProperty("org.apache.xml.security.ignoreLineBreaks", "illegal"); + System.setProperty("org.apache.xml.security.base64.ignoreLineBreaks", "illegal"); + System.setProperty("org.apache.xml.security.base64.lineSeparator", "illegal"); + System.setProperty("org.apache.xml.security.base64.lineLength", "illegal"); + + Class xmlUtilsClass = classLoader.loadClass(XMLUtils.class.getName()); + String encoded = encodeToString(xmlUtilsClass, data); + String elementValue = encodeElementValue(xmlUtilsClass, data); + String encodedWithStream = encodeUsingStream(xmlUtilsClass, data); + + assertThat(encoded, containsString("\r\n")); + OptionalInt maxLineLength = Arrays.stream(encoded.split("\r\n")).mapToInt(String::length).max(); + assertTrue(maxLineLength.isPresent()); + assertEquals(76, maxLineLength.getAsInt()); + + assertThat(elementValue, containsString(encoded)); + assertThat(elementValue, startsWith("\n")); + assertThat(elementValue, endsWith("\n")); + + assertEquals(encoded, encodedWithStream); + } + + private String encodeToString(Class xmlUtilsClass, byte[] bytes) throws ReflectiveOperationException { + return (String) xmlUtilsClass.getMethod("encodeToString", byte[].class).invoke(null, (Object) bytes); + } + + private String encodeElementValue(Class xmlUtilsClass, byte[] bytes) throws ReflectiveOperationException { + return (String) xmlUtilsClass.getMethod("encodeElementValue", byte[].class).invoke(null, (Object) bytes); + } + + private OutputStream encodeStream(Class xmlUtilsClass, OutputStream stream) throws ReflectiveOperationException { + return (OutputStream) xmlUtilsClass.getMethod("encodeStream", OutputStream.class).invoke(null, stream); + } + + private String encodeUsingStream(Class xmlUtilsClass, byte[] bytes) throws ReflectiveOperationException, IOException { + try (ByteArrayOutputStream encoded = new ByteArrayOutputStream(); + OutputStream raw = encodeStream(xmlUtilsClass, encoded)) { + raw.write(bytes); + raw.flush(); + return encoded.toString(StandardCharsets.US_ASCII); + } + } + + private static class ReloadingClassLoader extends ClassLoader { + private Collection classNames; + + public ReloadingClassLoader(ClassLoader parent, Collection> classes) { + super("TestClassLoader", parent); + this.classNames = classes.stream().map(Class::getName).collect(Collectors.toSet()); + } + + @Override + protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException { + if (classNames.contains(name)) { + Class clazz = findClass(name); + if (resolve) { + resolveClass(clazz); + } + return clazz; + } + return super.loadClass(name, resolve); + } + + @Override + protected Class findClass(String name) throws ClassNotFoundException { + if (classNames.contains(name)) { + Class parentLoadedClass = getParent().loadClass(name); + String resourceName = synthesizeClassName(parentLoadedClass) + ".class"; + byte[] classData; + try (InputStream in = parentLoadedClass.getResourceAsStream(resourceName)) { + if (in == null) { + throw new ClassNotFoundException("Could not load class " + name); + } + classData = in.readAllBytes(); + } catch (IOException e) { + throw new ClassNotFoundException("Could not load class " + name, e); + } + + return defineClass(name, classData, 0, classData.length); + } + throw new ClassNotFoundException("Class not found: " + name); + } + + private String synthesizeClassName(Class clazz) { + String name = clazz.getSimpleName(); + if (clazz.isMemberClass()) name = synthesizeClassName(clazz.getEnclosingClass()) + "$" + name; + return name; + } + } +} From 8a598d08ee846e348c1677ff26a42740aeb61d4d Mon Sep 17 00:00:00 2001 From: Ihor Kuzmanenko Date: Wed, 5 Nov 2025 15:35:41 +0200 Subject: [PATCH 2/6] updated XMLUtils in response to review comments --- .../AbstractEncryptOutputProcessor.java | 7 +- .../apache/xml/security/utils/XMLUtils.java | 95 +++++++++++-------- 2 files changed, 60 insertions(+), 42 deletions(-) diff --git a/src/main/java/org/apache/xml/security/stax/impl/processor/output/AbstractEncryptOutputProcessor.java b/src/main/java/org/apache/xml/security/stax/impl/processor/output/AbstractEncryptOutputProcessor.java index 7e5e5e293..611ada923 100644 --- a/src/main/java/org/apache/xml/security/stax/impl/processor/output/AbstractEncryptOutputProcessor.java +++ b/src/main/java/org/apache/xml/security/stax/impl/processor/output/AbstractEncryptOutputProcessor.java @@ -24,7 +24,12 @@ import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import java.security.spec.AlgorithmParameterSpec; -import java.util.*; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Deque; +import java.util.Iterator; +import java.util.List; +import java.util.Map; import java.util.Map.Entry; import javax.crypto.Cipher; diff --git a/src/main/java/org/apache/xml/security/utils/XMLUtils.java b/src/main/java/org/apache/xml/security/utils/XMLUtils.java index 18170ea19..057e7c327 100644 --- a/src/main/java/org/apache/xml/security/utils/XMLUtils.java +++ b/src/main/java/org/apache/xml/security/utils/XMLUtils.java @@ -57,7 +57,7 @@ * DOM and XML accessibility and comfort functions. * * @implNote - * Following system properties affect XML formatting: + * The following system properties affect XML formatting: *
    *
  • {@systemProperty org.apache.xml.security.ignoreLineBreaks} - ignores all line breaks, * making a single-line document. Overrides all other formatting options. Default: false
  • @@ -66,7 +66,7 @@ *
  • {@systemProperty org.apache.xml.security.base64.lineSeparator} - Sets the line separator sequence in base64Binary values. * Possible values: crlf, lf. Default: crlf
  • *
  • {@systemProperty org.apache.xml.security.base64.lineLength} - Sets maximum line length in base64Binary values. - * The value is rounded down to nearest multiple of 4. Values less than 4 are ignored. Default: 76
  • + * The value is rounded down to the nearest multiple of 4. Values less than 4 are ignored. Default: 76 *
*/ public final class XMLUtils { @@ -74,39 +74,14 @@ public final class XMLUtils { private static final Logger LOG = System.getLogger(XMLUtils.class.getName()); private static final String IGNORE_LINE_BREAKS_PROP = "org.apache.xml.security.ignoreLineBreaks"; - private static final String BASE64_IGNORE_LINE_BREAKS_PROP = "org.apache.xml.security.base64.ignoreLineBreaks"; - private static final String BASE64_LINE_SEPARATOR_PROP = "org.apache.xml.security.base64.lineSeparator"; - private static final String BASE64_LINE_LENGTH_PROP = "org.apache.xml.security.base64.lineLength"; private static boolean ignoreLineBreaks = AccessController.doPrivileged( (PrivilegedAction) () -> Boolean.getBoolean(IGNORE_LINE_BREAKS_PROP)); private static Base64FormattingOptions base64Formatting = - AccessController.doPrivileged((PrivilegedAction) () -> { - Base64FormattingOptions options = new Base64FormattingOptions(); - options.setIgnoreLineBreaks(Boolean.getBoolean(BASE64_IGNORE_LINE_BREAKS_PROP)); - - String lineSeparator = System.getProperty(BASE64_LINE_SEPARATOR_PROP); - if (lineSeparator != null) { - try { - options.setLineSeparator(Base64LineSeparator.valueOf(lineSeparator.toUpperCase())); - } catch (IllegalArgumentException e) { - LOG.log(Level.WARNING, "Illegal value of {0} property ignored: {1}", - BASE64_LINE_SEPARATOR_PROP, lineSeparator); - } - } - - Integer lineLength = Integer.getInteger(BASE64_LINE_LENGTH_PROP); - if (lineLength != null && lineLength >= 4) { - options.setLineLength(lineLength); - } else if (lineLength != null) { - LOG.log(Level.WARNING, "Illegal value of {0} property ignored: {1}", - BASE64_LINE_LENGTH_PROP, lineLength); - } - - return options; - }); + AccessController.doPrivileged( + (PrivilegedAction) () -> new Base64FormattingOptions()); private static Base64.Encoder base64Encoder = (ignoreLineBreaks || base64Formatting.isIgnoreLineBreaks()) ? Base64.getEncoder() : @@ -1152,33 +1127,71 @@ public static byte[] getBytes(BigInteger big, int bitlen) { * Aggregates formatting options for base64Binary values. */ static class Base64FormattingOptions { + private static final String BASE64_IGNORE_LINE_BREAKS_PROP = "org.apache.xml.security.base64.ignoreLineBreaks"; + private static final String BASE64_LINE_SEPARATOR_PROP = "org.apache.xml.security.base64.lineSeparator"; + private static final String BASE64_LINE_LENGTH_PROP = "org.apache.xml.security.base64.lineLength"; + private boolean ignoreLineBreaks = false; private Base64LineSeparator lineSeparator = Base64LineSeparator.CRLF; private int lineLength = 76; - public boolean isIgnoreLineBreaks() { - return ignoreLineBreaks; + /** + * Creates new formatting options by reading system properties. + */ + public Base64FormattingOptions() { + String ignoreLineBreaksProp = System.getProperty(BASE64_IGNORE_LINE_BREAKS_PROP); + ignoreLineBreaks = Boolean.parseBoolean(ignoreLineBreaksProp); + if (XMLUtils.ignoreLineBreaks && ignoreLineBreaksProp != null && !ignoreLineBreaks) { + LOG.log(Level.WARNING, "{0} property takes precedence over {1}, line breaks will be ignored", + IGNORE_LINE_BREAKS_PROP, BASE64_IGNORE_LINE_BREAKS_PROP); + } + + String lineSeparatorProp = System.getProperty(BASE64_LINE_SEPARATOR_PROP); + if (lineSeparatorProp != null) { + try { + lineSeparator = Base64LineSeparator.valueOf(lineSeparatorProp.toUpperCase()); + if (XMLUtils.ignoreLineBreaks || ignoreLineBreaks) { + LOG.log(Level.WARNING, "Property {0} has no effect since line breaks are ignored", + BASE64_LINE_SEPARATOR_PROP); + } + } catch (IllegalArgumentException e) { + LOG.log(Level.WARNING, "Illegal value of {0} property is ignored: {1}", + BASE64_LINE_SEPARATOR_PROP, lineSeparatorProp); + } + } + + String lineLengthProp = System.getProperty(BASE64_LINE_LENGTH_PROP); + if (lineLengthProp != null) { + try { + int lineLength = Integer.parseInt(lineLengthProp); + if (lineLength >= 4) { + this.lineLength = lineLength; + if (XMLUtils.ignoreLineBreaks || ignoreLineBreaks) { + LOG.log(Level.WARNING, "Property {0} has no effect since line breaks are ignored", + BASE64_LINE_LENGTH_PROP); + } + } else { + LOG.log(Level.WARNING, "Illegal value of {0} property is ignored: {1}", + BASE64_LINE_LENGTH_PROP, lineLengthProp); + } + } catch (NumberFormatException e) { + LOG.log(Level.WARNING, "Illegal value of {0} property is ignored: {1}", + BASE64_LINE_LENGTH_PROP, lineLengthProp); + } + } } - public void setIgnoreLineBreaks(boolean ignoreLineBreaks) { - this.ignoreLineBreaks = ignoreLineBreaks; + public boolean isIgnoreLineBreaks() { + return ignoreLineBreaks; } public Base64LineSeparator getLineSeparator() { return lineSeparator; } - public void setLineSeparator(Base64LineSeparator lineSeparator) { - this.lineSeparator = lineSeparator; - } - public int getLineLength() { return lineLength; } - - public void setLineLength(int lineLength) { - this.lineLength = lineLength; - } } enum Base64LineSeparator { From 8e67aafed3c69615bc8b642a425f34d1ba4d74ba Mon Sep 17 00:00:00 2001 From: Ihor Kuzmanenko Date: Fri, 7 Nov 2025 21:13:21 +0200 Subject: [PATCH 3/6] review: fixed access modifiers, added license and more comments in the test --- .../apache/xml/security/utils/XMLUtils.java | 4 +- .../xml/security/utils/XMLUtilsTest.java | 52 ++++++++++++++++++- 2 files changed, 52 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/apache/xml/security/utils/XMLUtils.java b/src/main/java/org/apache/xml/security/utils/XMLUtils.java index 057e7c327..a375044e5 100644 --- a/src/main/java/org/apache/xml/security/utils/XMLUtils.java +++ b/src/main/java/org/apache/xml/security/utils/XMLUtils.java @@ -1138,7 +1138,7 @@ static class Base64FormattingOptions { /** * Creates new formatting options by reading system properties. */ - public Base64FormattingOptions() { + Base64FormattingOptions() { String ignoreLineBreaksProp = System.getProperty(BASE64_IGNORE_LINE_BREAKS_PROP); ignoreLineBreaks = Boolean.parseBoolean(ignoreLineBreaksProp); if (XMLUtils.ignoreLineBreaks && ignoreLineBreaksProp != null && !ignoreLineBreaks) { @@ -1204,7 +1204,7 @@ enum Base64LineSeparator { this.bytes = bytes; } - public byte[] getBytes() { + byte[] getBytes() { return bytes; } } diff --git a/src/test/java/org/apache/xml/security/utils/XMLUtilsTest.java b/src/test/java/org/apache/xml/security/utils/XMLUtilsTest.java index b401291e9..c852ebd6c 100644 --- a/src/test/java/org/apache/xml/security/utils/XMLUtilsTest.java +++ b/src/test/java/org/apache/xml/security/utils/XMLUtilsTest.java @@ -1,3 +1,21 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ package org.apache.xml.security.utils; import org.junit.jupiter.api.AfterEach; @@ -16,6 +34,22 @@ import static org.hamcrest.Matchers.*; import static org.junit.jupiter.api.Assertions.*; +/** + * This test checks {@link XMLUtils} class methods, responsible for Base64 values formatting in XML documents. + * Since it is a utility class with static methods, and it is configured with system properties, + * we need to reload the class after system properties are set in each test case. + * This test uses a special implementation of {@link ClassLoader} to achieve this and calls {@code XMLUtils} methods + * using reflection. + * + * There are three methods producing Base64-encoded data in {@code XMLUtils}: + *
    + *
  • {@link XMLUtils#encodeToString(byte[])}
  • + *
  • {@link XMLUtils#encodeElementValue(byte[])}
  • + *
  • {@link XMLUtils#encodeStream(OutputStream)}
  • (creates a wrapper stream, which applies the same encoding + * as {@code encodeToString(byte[])}) + *
+ * In the tests, formatting of the outputs of these methods is checked. + */ public class XMLUtilsTest { private static final byte[] data = new byte[60]; // long enough for a line break in MIME encoding @@ -24,7 +58,7 @@ public class XMLUtilsTest { @BeforeEach public void createClassLoader() { - /* create custom classloader to reload class in each test */ + /* create custom classloader to reload XMLUtils class and its nested classes in each test */ ClassLoader parent = getClass().getClassLoader(); Collection> classesToReload = List.of( XMLUtils.class, @@ -32,6 +66,11 @@ public void createClassLoader() { XMLUtils.Base64LineSeparator.class ); classLoader = new ReloadingClassLoader(parent, classesToReload); + + /* + * XMLUtils instantiates XMLParserImpl, but its package is not exported, + * thus unavailable for the new classloader. + */ ModuleLayer.boot().findModule("org.apache.santuario.xmlsec").orElseThrow() .addOpens("org.apache.xml.security.parser", classLoader.getUnnamedModule()); } @@ -221,10 +260,19 @@ private String encodeUsingStream(Class xmlUtilsClass, byte[] bytes) throws Re } } + /** + * This implementation of {@code ClassLoader} reloads given classes from bytecode, + * even if they are already loaded by the parent class loader. + */ private static class ReloadingClassLoader extends ClassLoader { private Collection classNames; - public ReloadingClassLoader(ClassLoader parent, Collection> classes) { + /** + * Creates new class loader. + * @param parent Parent class loader. + * @param classes Set of classes to be forcefully reloaded + */ + private ReloadingClassLoader(ClassLoader parent, Collection> classes) { super("TestClassLoader", parent); this.classNames = classes.stream().map(Class::getName).collect(Collectors.toSet()); } From 908c97fd63612c0b8f794da979168c0fc6d17cb4 Mon Sep 17 00:00:00 2001 From: Ihor Kuzmanenko Date: Tue, 11 Nov 2025 18:10:45 +0200 Subject: [PATCH 4/6] added framework for formatting tests, refactored XMLUtilsTest * added FormattingTest annotation (JUnit tagging) * added FormattingChecker interface, various implementations for different formatting configurations and a factory to get appropriate implementation * added formatting options properties sets and multiple executions for Surefire plugin * refactored XMLUtilsTest: made it a @FormattingTest, removed hacks with classloader --- pom.xml | 74 +++++ .../CustomBase64FormattingChecker.java | 74 +++++ .../formatting/FormattingChecker.java | 53 +++ .../formatting/FormattingCheckerFactory.java | 45 +++ .../security/formatting/FormattingTest.java | 37 +++ .../formatting/NoBase64LineBreaksChecker.java | 43 +++ .../formatting/NoLineBreaksChecker.java | 45 +++ .../xml/security/utils/XMLUtilsTest.java | 309 ++++-------------- .../base64-custom-formatting.properties | 2 + ...ore-base64-line-breaks-override.properties | 4 + .../ignore-base64-line-breaks.properties | 1 + .../ignore-line-breaks-override.properties | 5 + .../formatting/ignore-line-breaks.properties | 1 + .../resources/formatting/illegal.properties | 4 + 14 files changed, 454 insertions(+), 243 deletions(-) create mode 100644 src/test/java/org/apache/xml/security/formatting/CustomBase64FormattingChecker.java create mode 100644 src/test/java/org/apache/xml/security/formatting/FormattingChecker.java create mode 100644 src/test/java/org/apache/xml/security/formatting/FormattingCheckerFactory.java create mode 100644 src/test/java/org/apache/xml/security/formatting/FormattingTest.java create mode 100644 src/test/java/org/apache/xml/security/formatting/NoBase64LineBreaksChecker.java create mode 100644 src/test/java/org/apache/xml/security/formatting/NoLineBreaksChecker.java create mode 100644 src/test/resources/formatting/base64-custom-formatting.properties create mode 100644 src/test/resources/formatting/ignore-base64-line-breaks-override.properties create mode 100644 src/test/resources/formatting/ignore-base64-line-breaks.properties create mode 100644 src/test/resources/formatting/ignore-line-breaks-override.properties create mode 100644 src/test/resources/formatting/ignore-line-breaks.properties create mode 100644 src/test/resources/formatting/illegal.properties diff --git a/pom.xml b/pom.xml index d9e9b14e5..4e5610a02 100644 --- a/pom.xml +++ b/pom.xml @@ -551,6 +551,80 @@ en_US:us + + + formatting-ignore-line-breaks + + test + + + formattingTest + + ${project.build.testOutputDirectory}/formatting/ignore-line-breaks.properties + + + + + formatting-ignore-line-breaks-override + + test + + + formattingTest + + ${project.build.testOutputDirectory}/formatting/ignore-line-breaks-override.properties + + + + + formatting-ignore-base64-line-breaks + + test + + + formattingTest + + ${project.build.testOutputDirectory}/formatting/ignore-base64-line-breaks.properties + + + + + formatting-ignore-base64-line-breaks-override + + test + + + formattingTest + + ${project.build.testOutputDirectory}/formatting/ignore-base64-line-breaks-override.properties + + + + + formatting-base64-custom-formatting + + test + + + formattingTest + + ${project.build.testOutputDirectory}/formatting/base64-custom-formatting.properties + + + + + formatting-illegal + + test + + + formattingTest + + ${project.build.testOutputDirectory}/formatting/illegal.properties + + + + maven-failsafe-plugin diff --git a/src/test/java/org/apache/xml/security/formatting/CustomBase64FormattingChecker.java b/src/test/java/org/apache/xml/security/formatting/CustomBase64FormattingChecker.java new file mode 100644 index 000000000..eb2706f90 --- /dev/null +++ b/src/test/java/org/apache/xml/security/formatting/CustomBase64FormattingChecker.java @@ -0,0 +1,74 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.xml.security.formatting; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.*; + +/** + * Checks that XML document is 'pretty-printed', including Base64 values. + */ +public class CustomBase64FormattingChecker implements FormattingChecker { + private int lineLength; + private String lineSeparatorRegex; + + /** + * Creates new checker. + * @param lineLength Expected base64 maximum line length + * @param lineSeparatorRegex Regex matching line separator used in Base64 values + */ + public CustomBase64FormattingChecker(int lineLength, String lineSeparatorRegex) { + this.lineLength = lineLength; + this.lineSeparatorRegex = lineSeparatorRegex; + } + + @Override + public void checkDocument(String document) { + assertThat(document, containsString("\n")); + } + + @Override + public void checkBase64Value(String value) { + String[] lines = value.split(lineSeparatorRegex); + if (lines.length == 0) return; + + for (int i = 0; i < lines.length - 1; ++i) { + assertThat(lines[i], matchesPattern(BASE64_PATTERN)); + assertEquals(lineLength, lines[i].length()); + } + + assertThat(lines[lines.length - 1], matchesPattern(BASE64_PATTERN)); + assertThat(lines[lines.length - 1].length(), lessThanOrEqualTo(lineLength)); + } + + @Override + public void checkBase64ValueWithSpacing(String value) { + /* spacing is added only if the value has multiple lines */ + if (value.length() <= lineLength) { + assertThat(value, matchesRegex(BASE64_PATTERN)); + return; + } + + assertThat(value.length(), greaterThanOrEqualTo(2)); + assertThat(value, startsWith("\n")); + assertThat(value, endsWith("\n")); + checkBase64Value(value.substring(1, value.length() - 1)); + } +} diff --git a/src/test/java/org/apache/xml/security/formatting/FormattingChecker.java b/src/test/java/org/apache/xml/security/formatting/FormattingChecker.java new file mode 100644 index 000000000..3469e849d --- /dev/null +++ b/src/test/java/org/apache/xml/security/formatting/FormattingChecker.java @@ -0,0 +1,53 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.xml.security.formatting; + +import java.util.regex.Pattern; + +/** + * Checks document formatting where output depends on formatting options. + * Base64 values can be treated in two ways: relatively long values can have additional line breaks + * to separate them from element tags. + */ +public interface FormattingChecker { + /** + * This pattern checks if a string contains only characters from the Base64 alphabet, including padding. + */ + Pattern BASE64_PATTERN = Pattern.compile("^[A-Za-z0-9+/=]*$"); + + /** + * Checks the formatting of the whole document. + * @param document XML document as string + * + * @implSpec It is assumed that the document contains at least one nested element. + */ + void checkDocument(String document); + + /** + * Checks encoded base64 element/attribute value. + * @param value Element value + */ + void checkBase64Value(String value); + + /** + * Checks encoded base64 element value with additional spacing. + * @param value Element value + */ + void checkBase64ValueWithSpacing(String value); +} diff --git a/src/test/java/org/apache/xml/security/formatting/FormattingCheckerFactory.java b/src/test/java/org/apache/xml/security/formatting/FormattingCheckerFactory.java new file mode 100644 index 000000000..9a0104682 --- /dev/null +++ b/src/test/java/org/apache/xml/security/formatting/FormattingCheckerFactory.java @@ -0,0 +1,45 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.xml.security.formatting; + +/** + * Creates formatting checker depending on system properties which define document formatting. + */ +public class FormattingCheckerFactory { + private static final int DEFAULT_BASE64_LINE_LENGTH = 76; + + /** + * Gets formatting checker according to system properties. + * @return Formatting checker implementation + */ + public static FormattingChecker getFormattingChecker() { + if (Boolean.getBoolean("org.apache.xml.security.ignoreLineBreaks")) { + /* overrides all Base64 formatting options */ + return new NoLineBreaksChecker(); + } else if (Boolean.getBoolean("org.apache.xml.security.base64.ignoreLineBreaks")) { + return new NoBase64LineBreaksChecker(); + } else { + int lineLength = Integer.getInteger("org.apache.xml.security.base64.lineLength", + DEFAULT_BASE64_LINE_LENGTH); + String lineSeparator = System.getProperty("org.apache.xml.security.base64.lineSeparator"); + String lineSeparatorRegex = "lf".equalsIgnoreCase(lineSeparator) ? "\\n" : "\\r\\n"; + return new CustomBase64FormattingChecker(lineLength, lineSeparatorRegex); + } + } +} diff --git a/src/test/java/org/apache/xml/security/formatting/FormattingTest.java b/src/test/java/org/apache/xml/security/formatting/FormattingTest.java new file mode 100644 index 000000000..3b53146ec --- /dev/null +++ b/src/test/java/org/apache/xml/security/formatting/FormattingTest.java @@ -0,0 +1,37 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.xml.security.formatting; + +import org.junit.jupiter.api.Tag; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Marks formatting tests which are typically run in different configurations set by system properties. + * See {@link org.apache.xml.security.utils.XMLUtils} for formatting options. + * Please use {@link FormattingCheckerFactory} to get an appropriate checker for the test. + */ +@Target({ElementType.TYPE, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Tag("formattingTest") +public @interface FormattingTest { +} diff --git a/src/test/java/org/apache/xml/security/formatting/NoBase64LineBreaksChecker.java b/src/test/java/org/apache/xml/security/formatting/NoBase64LineBreaksChecker.java new file mode 100644 index 000000000..f63f19911 --- /dev/null +++ b/src/test/java/org/apache/xml/security/formatting/NoBase64LineBreaksChecker.java @@ -0,0 +1,43 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.xml.security.formatting; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.matchesPattern; + +/** + * Checks the document is 'pretty-printed', but base64 values are without line breaks. + */ +public class NoBase64LineBreaksChecker implements FormattingChecker { + @Override + public void checkDocument(String document) { + assertThat(document, containsString("\n")); + } + + @Override + public void checkBase64Value(String value) { + assertThat(value, matchesPattern(BASE64_PATTERN)); + } + + @Override + public void checkBase64ValueWithSpacing(String value) { + assertThat(value, matchesPattern(BASE64_PATTERN)); + } +} diff --git a/src/test/java/org/apache/xml/security/formatting/NoLineBreaksChecker.java b/src/test/java/org/apache/xml/security/formatting/NoLineBreaksChecker.java new file mode 100644 index 000000000..95587759c --- /dev/null +++ b/src/test/java/org/apache/xml/security/formatting/NoLineBreaksChecker.java @@ -0,0 +1,45 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.xml.security.formatting; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.not; +import static org.hamcrest.Matchers.matchesPattern; + +/** + * Checks there are no line breaks in the document. + */ +public class NoLineBreaksChecker implements FormattingChecker { + @Override + public void checkDocument(String document) { + assertThat(document, not(containsString("\n"))); + assertThat(document, not(containsString("\r"))); + } + + @Override + public void checkBase64Value(String value) { + assertThat(value, matchesPattern(BASE64_PATTERN)); + } + + @Override + public void checkBase64ValueWithSpacing(String value) { + assertThat(value, matchesPattern(BASE64_PATTERN)); + } +} diff --git a/src/test/java/org/apache/xml/security/utils/XMLUtilsTest.java b/src/test/java/org/apache/xml/security/utils/XMLUtilsTest.java index c852ebd6c..18cc1119b 100644 --- a/src/test/java/org/apache/xml/security/utils/XMLUtilsTest.java +++ b/src/test/java/org/apache/xml/security/utils/XMLUtilsTest.java @@ -18,28 +18,20 @@ */ package org.apache.xml.security.utils; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; +import org.apache.xml.security.formatting.FormattingChecker; +import org.apache.xml.security.formatting.FormattingCheckerFactory; +import org.apache.xml.security.formatting.FormattingTest; import org.junit.jupiter.api.Test; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; +import java.io.*; import java.nio.charset.StandardCharsets; -import java.util.*; -import java.util.stream.Collectors; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.*; import static org.junit.jupiter.api.Assertions.*; /** * This test checks {@link XMLUtils} class methods, responsible for Base64 values formatting in XML documents. - * Since it is a utility class with static methods, and it is configured with system properties, - * we need to reload the class after system properties are set in each test case. - * This test uses a special implementation of {@link ClassLoader} to achieve this and calls {@code XMLUtils} methods - * using reflection. + * This is a {@link FormattingTest}, it is expected to be run with different system properties + * to check various formatting configurations. * * There are three methods producing Base64-encoded data in {@code XMLUtils}: *
    @@ -48,271 +40,102 @@ *
  • {@link XMLUtils#encodeStream(OutputStream)}
  • (creates a wrapper stream, which applies the same encoding * as {@code encodeToString(byte[])}) *
- * In the tests, formatting of the outputs of these methods is checked. + * Output of the first two methods is checked using an appropriate {@link FormattingChecker} implementation. + * The result of stream encoding is compared to the output of {@code encodeToString} method. + * + * There are also tests, which check that the corresponding decoding methods can process Base64-encoded data with any + * formatting regardless of formatting options. */ +@FormattingTest public class XMLUtilsTest { - private static final byte[] data = new byte[60]; // long enough for a line break in MIME encoding - - private Properties backup; - private ClassLoader classLoader; - - @BeforeEach - public void createClassLoader() { - /* create custom classloader to reload XMLUtils class and its nested classes in each test */ - ClassLoader parent = getClass().getClassLoader(); - Collection> classesToReload = List.of( - XMLUtils.class, - XMLUtils.Base64FormattingOptions.class, - XMLUtils.Base64LineSeparator.class - ); - classLoader = new ReloadingClassLoader(parent, classesToReload); - /* - * XMLUtils instantiates XMLParserImpl, but its package is not exported, - * thus unavailable for the new classloader. - */ - ModuleLayer.boot().findModule("org.apache.santuario.xmlsec").orElseThrow() - .addOpens("org.apache.xml.security.parser", classLoader.getUnnamedModule()); - } - - @BeforeEach - public void backupProperties() { - backup = new Properties(); - backup.putAll(System.getProperties()); - } + private FormattingChecker formattingChecker = FormattingCheckerFactory.getFormattingChecker(); - @AfterEach - public void restoreProperties() { - System.setProperties(backup); - } + /* Base64 encoding of the following bytes is: AQIDBAUGBwg= */ + private static final byte[] TEST_DATA = new byte[]{ 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08 }; @Test - public void testAllPropertiesUnset() throws ReflectiveOperationException, IOException { - System.clearProperty("org.apache.xml.security.ignoreLineBreaks"); - System.clearProperty("org.apache.xml.security.base64.ignoreLineBreaks"); - System.clearProperty("org.apache.xml.security.base64.lineSeparator"); - System.clearProperty("org.apache.xml.security.base64.lineLength"); - - Class xmlUtilsClass = classLoader.loadClass(XMLUtils.class.getName()); - String encoded = encodeToString(xmlUtilsClass, data); - String elementValue = encodeElementValue(xmlUtilsClass, data); - String encodedWithStream = encodeUsingStream(xmlUtilsClass, data); - - assertThat(encoded, containsString("\r\n")); - OptionalInt maxLineLength = Arrays.stream(encoded.split("\r\n")).mapToInt(String::length).max(); - assertTrue(maxLineLength.isPresent()); - assertEquals(76, maxLineLength.getAsInt()); - - assertThat(elementValue, containsString(encoded)); - assertThat(elementValue, startsWith("\n")); - assertThat(elementValue, endsWith("\n")); - - assertEquals(encoded, encodedWithStream); + public void testEncodeToString() { + byte[] data = new byte[60]; // long enough for a line break in MIME encoding + String encoded = XMLUtils.encodeToString(data); + formattingChecker.checkBase64Value(encoded); } @Test - public void testIgnoreLineBreaksSet() throws ReflectiveOperationException, IOException { - System.setProperty("org.apache.xml.security.ignoreLineBreaks", "true"); - System.clearProperty("org.apache.xml.security.base64.ignoreLineBreaks"); - System.clearProperty("org.apache.xml.security.base64.lineSeparator"); - System.clearProperty("org.apache.xml.security.base64.lineLength"); - - Class xmlUtilsClass = classLoader.loadClass(XMLUtils.class.getName()); - String encoded = encodeToString(xmlUtilsClass, data); - String elementValue = encodeElementValue(xmlUtilsClass, data); - String encodedWithStream = encodeUsingStream(xmlUtilsClass, data); - - assertThat(encoded, not(containsString("\r\n"))); - assertThat(encoded, not(containsString("\n"))); - assertThat(elementValue, not(containsString("\r\n"))); - assertThat(elementValue, not(containsString("\n"))); - - assertEquals(encoded, encodedWithStream); + public void testEncodeToStringShort() { + byte[] data = new byte[8]; + String encoded = XMLUtils.encodeToString(data); + formattingChecker.checkBase64Value(encoded); } @Test - public void testIgnoreLineBreaksTakesPrecedence() throws ReflectiveOperationException, IOException { - System.setProperty("org.apache.xml.security.ignoreLineBreaks", "true"); - System.setProperty("org.apache.xml.security.base64.ignoreLineBreaks", "false"); - System.setProperty("org.apache.xml.security.base64.lineSeparator", "crlf"); - System.setProperty("org.apache.xml.security.base64.lineLength", "40"); - - Class xmlUtilsClass = classLoader.loadClass(XMLUtils.class.getName()); - String encoded = encodeToString(xmlUtilsClass, data); - String elementValue = encodeElementValue(xmlUtilsClass, data); - String encodedWithStream = encodeUsingStream(xmlUtilsClass, data); - - assertThat(encoded, not(containsString("\r\n"))); - assertThat(encoded, not(containsString("\n"))); - assertThat(elementValue, not(containsString("\r\n"))); - assertThat(elementValue, not(containsString("\n"))); - - assertEquals(encoded, encodedWithStream); + public void testEncodeElementValue() { + byte[] data = new byte[60]; // long enough for a line break in MIME encoding + String encoded = XMLUtils.encodeElementValue(data); + formattingChecker.checkBase64ValueWithSpacing(encoded); } @Test - public void testBase64IgnoreLineBreaksSet() throws ReflectiveOperationException, IOException { - System.clearProperty("org.apache.xml.security.ignoreLineBreaks"); - System.setProperty("org.apache.xml.security.base64.ignoreLineBreaks", "true"); - System.clearProperty("org.apache.xml.security.base64.lineSeparator"); - System.clearProperty("org.apache.xml.security.base64.lineLength"); - - Class xmlUtilsClass = classLoader.loadClass(XMLUtils.class.getName()); - String encoded = encodeToString(xmlUtilsClass, data); - String elementValue = encodeElementValue(xmlUtilsClass, data); - String encodedWithStream = encodeUsingStream(xmlUtilsClass, data); - - assertThat(encoded, not(containsString("\r\n"))); - assertThat(encoded, not(containsString("\n"))); - assertThat(elementValue, not(containsString("\r\n"))); - assertThat(elementValue, not(containsString("\n"))); - - assertEquals(encoded, encodedWithStream); + public void testEncodeElementValueShort() { + byte[] data = new byte[8]; + String encoded = XMLUtils.encodeElementValue(data); + formattingChecker.checkBase64ValueWithSpacing(encoded); } @Test - public void testBase64IgnoreLineBreaksTakesPrecedence() throws ReflectiveOperationException, IOException { - System.clearProperty("org.apache.xml.security.ignoreLineBreaks"); - System.setProperty("org.apache.xml.security.base64.ignoreLineBreaks", "true"); - System.setProperty("org.apache.xml.security.base64.lineSeparator", "crlf"); - System.setProperty("org.apache.xml.security.base64.lineLength", "40"); - - Class xmlUtilsClass = classLoader.loadClass(XMLUtils.class.getName()); - String encoded = encodeToString(xmlUtilsClass, data); - String elementValue = encodeElementValue(xmlUtilsClass, data); - String encodedWithStream = encodeUsingStream(xmlUtilsClass, data); - - assertThat(encoded, not(containsString("\r\n"))); - assertThat(encoded, not(containsString("\n"))); - assertThat(elementValue, not(containsString("\r\n"))); - assertThat(elementValue, not(containsString("\n"))); + public void testEncodeUsingStream() throws IOException { + byte[] data = new byte[60]; + String expected = XMLUtils.encodeToString(data); + String encodedWithStream; + try (ByteArrayOutputStream encoded = new ByteArrayOutputStream(); + OutputStream raw = XMLUtils.encodeStream(encoded)) { + raw.write(data); + raw.flush(); + encodedWithStream = encoded.toString(StandardCharsets.US_ASCII); + } - assertEquals(encoded, encodedWithStream); + assertEquals(expected, encodedWithStream); } @Test - public void testBase64CustomFormatting() throws ReflectiveOperationException, IOException { - System.clearProperty("org.apache.xml.security.ignoreLineBreaks"); - System.clearProperty("org.apache.xml.security.base64.ignoreLineBreaks"); - System.setProperty("org.apache.xml.security.base64.lineSeparator", "lf"); - System.setProperty("org.apache.xml.security.base64.lineLength", "40"); - - Class xmlUtilsClass = classLoader.loadClass(XMLUtils.class.getName()); - String encoded = encodeToString(xmlUtilsClass, data); - String elementValue = encodeElementValue(xmlUtilsClass, data); - String encodedWithStream = encodeUsingStream(xmlUtilsClass, data); + public void decodeNoLineBreaks() { + String encoded = "AQIDBAUGBwg="; - assertThat(encoded, not(containsString("\r\n"))); - assertThat(encoded, containsString("\n")); - OptionalInt maxLineLength = Arrays.stream(encoded.split("\n")).mapToInt(String::length).max(); - assertTrue(maxLineLength.isPresent()); - assertEquals(40, maxLineLength.getAsInt()); + byte[] data = XMLUtils.decode(encoded); + assertArrayEquals(TEST_DATA, data); - assertThat(elementValue, containsString(encoded)); - assertThat(elementValue, startsWith("\n")); - assertThat(elementValue, endsWith("\n")); - - assertEquals(encoded, encodedWithStream); + data = XMLUtils.decode(encoded.getBytes(StandardCharsets.US_ASCII)); + assertArrayEquals(TEST_DATA, data); } @Test - public void testIllegalPropertiesAreIgnored() throws ReflectiveOperationException, IOException { - System.setProperty("org.apache.xml.security.ignoreLineBreaks", "illegal"); - System.setProperty("org.apache.xml.security.base64.ignoreLineBreaks", "illegal"); - System.setProperty("org.apache.xml.security.base64.lineSeparator", "illegal"); - System.setProperty("org.apache.xml.security.base64.lineLength", "illegal"); - - Class xmlUtilsClass = classLoader.loadClass(XMLUtils.class.getName()); - String encoded = encodeToString(xmlUtilsClass, data); - String elementValue = encodeElementValue(xmlUtilsClass, data); - String encodedWithStream = encodeUsingStream(xmlUtilsClass, data); - - assertThat(encoded, containsString("\r\n")); - OptionalInt maxLineLength = Arrays.stream(encoded.split("\r\n")).mapToInt(String::length).max(); - assertTrue(maxLineLength.isPresent()); - assertEquals(76, maxLineLength.getAsInt()); + public void decodeCrlfLineBreaks() { + String encoded = "AQIDBAUG\r\nBwg="; - assertThat(elementValue, containsString(encoded)); - assertThat(elementValue, startsWith("\n")); - assertThat(elementValue, endsWith("\n")); + byte[] data = XMLUtils.decode(encoded); + assertArrayEquals(TEST_DATA, data); - assertEquals(encoded, encodedWithStream); + data = XMLUtils.decode(encoded.getBytes(StandardCharsets.US_ASCII)); + assertArrayEquals(TEST_DATA, data); } - private String encodeToString(Class xmlUtilsClass, byte[] bytes) throws ReflectiveOperationException { - return (String) xmlUtilsClass.getMethod("encodeToString", byte[].class).invoke(null, (Object) bytes); - } - - private String encodeElementValue(Class xmlUtilsClass, byte[] bytes) throws ReflectiveOperationException { - return (String) xmlUtilsClass.getMethod("encodeElementValue", byte[].class).invoke(null, (Object) bytes); - } + @Test + public void decodeLfLineBreaks() { + String encoded = "AQIDBAUG\nBwg="; - private OutputStream encodeStream(Class xmlUtilsClass, OutputStream stream) throws ReflectiveOperationException { - return (OutputStream) xmlUtilsClass.getMethod("encodeStream", OutputStream.class).invoke(null, stream); - } + byte[] data = XMLUtils.decode(encoded); + assertArrayEquals(TEST_DATA, data); - private String encodeUsingStream(Class xmlUtilsClass, byte[] bytes) throws ReflectiveOperationException, IOException { - try (ByteArrayOutputStream encoded = new ByteArrayOutputStream(); - OutputStream raw = encodeStream(xmlUtilsClass, encoded)) { - raw.write(bytes); - raw.flush(); - return encoded.toString(StandardCharsets.US_ASCII); - } + data = XMLUtils.decode(encoded.getBytes(StandardCharsets.US_ASCII)); + assertArrayEquals(TEST_DATA, data); } - /** - * This implementation of {@code ClassLoader} reloads given classes from bytecode, - * even if they are already loaded by the parent class loader. - */ - private static class ReloadingClassLoader extends ClassLoader { - private Collection classNames; - - /** - * Creates new class loader. - * @param parent Parent class loader. - * @param classes Set of classes to be forcefully reloaded - */ - private ReloadingClassLoader(ClassLoader parent, Collection> classes) { - super("TestClassLoader", parent); - this.classNames = classes.stream().map(Class::getName).collect(Collectors.toSet()); - } - - @Override - protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException { - if (classNames.contains(name)) { - Class clazz = findClass(name); - if (resolve) { - resolveClass(clazz); - } - return clazz; - } - return super.loadClass(name, resolve); - } - - @Override - protected Class findClass(String name) throws ClassNotFoundException { - if (classNames.contains(name)) { - Class parentLoadedClass = getParent().loadClass(name); - String resourceName = synthesizeClassName(parentLoadedClass) + ".class"; - byte[] classData; - try (InputStream in = parentLoadedClass.getResourceAsStream(resourceName)) { - if (in == null) { - throw new ClassNotFoundException("Could not load class " + name); - } - classData = in.readAllBytes(); - } catch (IOException e) { - throw new ClassNotFoundException("Could not load class " + name, e); - } - - return defineClass(name, classData, 0, classData.length); - } - throw new ClassNotFoundException("Class not found: " + name); - } + @Test + public void decodeStream() throws IOException { + byte[] encodedBytes = "AQIDBAUGBwg=".getBytes(StandardCharsets.US_ASCII); - private String synthesizeClassName(Class clazz) { - String name = clazz.getSimpleName(); - if (clazz.isMemberClass()) name = synthesizeClassName(clazz.getEnclosingClass()) + "$" + name; - return name; + try (InputStream decoded = XMLUtils.decodeStream(new ByteArrayInputStream(encodedBytes))) { + assertArrayEquals(TEST_DATA, decoded.readAllBytes()); } } } diff --git a/src/test/resources/formatting/base64-custom-formatting.properties b/src/test/resources/formatting/base64-custom-formatting.properties new file mode 100644 index 000000000..438443a61 --- /dev/null +++ b/src/test/resources/formatting/base64-custom-formatting.properties @@ -0,0 +1,2 @@ +org.apache.xml.security.base64.lineSeparator=lf +org.apache.xml.security.base64.lineLength=40 diff --git a/src/test/resources/formatting/ignore-base64-line-breaks-override.properties b/src/test/resources/formatting/ignore-base64-line-breaks-override.properties new file mode 100644 index 000000000..139c935e1 --- /dev/null +++ b/src/test/resources/formatting/ignore-base64-line-breaks-override.properties @@ -0,0 +1,4 @@ +org.apache.xml.security.base64.ignoreLineBreaks=true +# following properties are ignored, as base64.ignoreLineBreaks takes precedence +org.apache.xml.security.base64.lineSeparator=lf +org.apache.xml.security.base64.lineLength=40 diff --git a/src/test/resources/formatting/ignore-base64-line-breaks.properties b/src/test/resources/formatting/ignore-base64-line-breaks.properties new file mode 100644 index 000000000..de3d6b9e4 --- /dev/null +++ b/src/test/resources/formatting/ignore-base64-line-breaks.properties @@ -0,0 +1 @@ +org.apache.xml.security.base64.ignoreLineBreaks=true diff --git a/src/test/resources/formatting/ignore-line-breaks-override.properties b/src/test/resources/formatting/ignore-line-breaks-override.properties new file mode 100644 index 000000000..aed594f41 --- /dev/null +++ b/src/test/resources/formatting/ignore-line-breaks-override.properties @@ -0,0 +1,5 @@ +org.apache.xml.security.ignoreLineBreaks=true +# following properties are ignored, as ignoreLineBreaks takes precedence +org.apache.xml.security.base64.ignoreLineBreaks=false +org.apache.xml.security.base64.lineSeparator=lf +org.apache.xml.security.base64.lineLength=40 diff --git a/src/test/resources/formatting/ignore-line-breaks.properties b/src/test/resources/formatting/ignore-line-breaks.properties new file mode 100644 index 000000000..c17eff53e --- /dev/null +++ b/src/test/resources/formatting/ignore-line-breaks.properties @@ -0,0 +1 @@ +org.apache.xml.security.ignoreLineBreaks=true diff --git a/src/test/resources/formatting/illegal.properties b/src/test/resources/formatting/illegal.properties new file mode 100644 index 000000000..ad93a7483 --- /dev/null +++ b/src/test/resources/formatting/illegal.properties @@ -0,0 +1,4 @@ +org.apache.xml.security.ignoreLineBreaks=illegal_value +org.apache.xml.security.base64.ignoreLineBreaks=illegal_value +org.apache.xml.security.base64.lineSeparator=illegal_value +org.apache.xml.security.base64.lineLength=illegal_value From 9e3fdac5f6930ebb721928072c63ce2cb98ad145 Mon Sep 17 00:00:00 2001 From: Ihor Kuzmanenko Date: Sat, 6 Dec 2025 16:30:26 +0200 Subject: [PATCH 5/6] added high-level formatting tests for signature and encryption --- .../encryption/EncryptionFormattingTest.java | 168 +++++++++++++++ .../signature/SignatureFormattingTest.java | 178 ++++++++++++++++ .../encryption/EncryptionFormattingTest.java | 193 ++++++++++++++++++ .../signature/SignatureFormattingTest.java | 181 ++++++++++++++++ .../apache/xml/security/samples/input/rsa.p12 | Bin 0 -> 2612 bytes 5 files changed, 720 insertions(+) create mode 100644 src/test/java/org/apache/xml/security/test/dom/encryption/EncryptionFormattingTest.java create mode 100644 src/test/java/org/apache/xml/security/test/dom/signature/SignatureFormattingTest.java create mode 100644 src/test/java/org/apache/xml/security/test/stax/encryption/EncryptionFormattingTest.java create mode 100644 src/test/java/org/apache/xml/security/test/stax/signature/SignatureFormattingTest.java create mode 100644 src/test/resources/org/apache/xml/security/samples/input/rsa.p12 diff --git a/src/test/java/org/apache/xml/security/test/dom/encryption/EncryptionFormattingTest.java b/src/test/java/org/apache/xml/security/test/dom/encryption/EncryptionFormattingTest.java new file mode 100644 index 000000000..2282b9d5f --- /dev/null +++ b/src/test/java/org/apache/xml/security/test/dom/encryption/EncryptionFormattingTest.java @@ -0,0 +1,168 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.xml.security.test.dom.encryption; + +import org.apache.xml.security.Init; +import org.apache.xml.security.encryption.EncryptedData; +import org.apache.xml.security.encryption.EncryptedKey; +import org.apache.xml.security.encryption.XMLCipher; +import org.apache.xml.security.formatting.FormattingChecker; +import org.apache.xml.security.formatting.FormattingCheckerFactory; +import org.apache.xml.security.formatting.FormattingTest; +import org.apache.xml.security.keys.KeyInfo; +import org.apache.xml.security.test.dom.DSNamespaceContext; +import org.apache.xml.security.test.dom.TestUtils; +import org.apache.xml.security.utils.XMLUtils; +import org.junit.jupiter.api.Test; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.NodeList; + +import javax.crypto.spec.SecretKeySpec; +import javax.xml.xpath.XPath; +import javax.xml.xpath.XPathConstants; +import javax.xml.xpath.XPathFactory; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.security.GeneralSecurityException; +import java.security.Key; +import java.security.KeyStore; +import java.security.cert.Certificate; +import java.util.Map; +import java.util.Random; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * This is a {@link FormattingTest}, it is expected to be run with different system properties + * to check various formatting configurations. + * + * The test uses AES-256-GCM encryption with RSA-OAEP key wrapping to generate a document containing encrypted data + * and data encryption key. + */ +@FormattingTest +public class EncryptionFormattingTest { + private final Random random = new Random(); + private final FormattingChecker formattingChecker; + private KeyStore keyStore; + private XPath xpath; + + public EncryptionFormattingTest() throws Exception { + Init.init(); + formattingChecker = FormattingCheckerFactory.getFormattingChecker(); + keyStore = KeyStore.getInstance("PKCS12"); + try (InputStream in = getClass() + .getResourceAsStream("/org/apache/xml/security/samples/input/rsa.p12")) { + keyStore.load(in, "xmlsecurity".toCharArray()); + } catch (IOException | GeneralSecurityException e) { + fail("Cannot load test keystore", e); + } + + XPathFactory xPathFactory = XPathFactory.newInstance(); + xpath = xPathFactory.newXPath(); + xpath.setNamespaceContext(new DSNamespaceContext(Map.of( + "xenc", "http://www.w3.org/2001/04/xmlenc#" + ))); + } + + @Test + public void testEncryptedFormatting() throws Exception { + /* this test checks formatting of base64binary values */ + byte[] testData = new byte[128]; // long enough for line breaks + random.nextBytes(testData); + + Document doc = createDocument(testData); + + String str; + try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) { + XMLUtils.outputDOM(doc, baos); + str = baos.toString(StandardCharsets.UTF_8); + formattingChecker.checkDocument(str); + } + + NodeList elements = (NodeList) xpath.evaluate("//xenc:CipherData", doc, XPathConstants.NODESET); + assertEquals(2, elements.getLength()); + formattingChecker.checkBase64Value(elements.item(0).getTextContent()); + formattingChecker.checkBase64Value(elements.item(1).getTextContent()); + } + + @Test + public void testEncryptDecrypt() throws Exception { + /* this test ensures that the encrypted data can be processed with various formatting settings */ + byte[] testData = new byte[128]; // long enough for line breaks + random.nextBytes(testData); + + Document doc = createDocument(testData); + Element encryptedKeyElement = + (Element) xpath.evaluate("//xenc:EncryptedKey[1]", doc, XPathConstants.NODE); + Element encryptedDataElement = + (Element) xpath.evaluate("//xenc:EncryptedData[1]", doc, XPathConstants.NODE); + + Key kek = keyStore.getKey("test", "xmlsecurity".toCharArray()); + XMLCipher keyCipher = XMLCipher.getInstance("http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p", + null, "http://www.w3.org/2001/04/xmlenc#sha512"); + keyCipher.init(XMLCipher.UNWRAP_MODE, kek); + EncryptedKey encryptedKey = keyCipher.loadEncryptedKey(doc, encryptedKeyElement); + Key sessionKey = keyCipher.decryptKey(encryptedKey, "http://www.w3.org/2009/xmlenc11#aes256-gcm"); + + XMLCipher dataCipher = XMLCipher.getInstance("http://www.w3.org/2009/xmlenc11#aes256-gcm"); + dataCipher.init(XMLCipher.DECRYPT_MODE, sessionKey); + byte[] decryptedData = dataCipher.decryptToByteArray(encryptedDataElement); + + assertArrayEquals(testData, decryptedData); + } + + private Key generateSessionKey() { + byte[] keyBytes = new byte[32]; + random.nextBytes(keyBytes); + return new SecretKeySpec(keyBytes, "AES"); + } + + private Document createDocument(byte[] data) throws Exception { + Document doc = TestUtils.newDocument(); + Key sessionKey = generateSessionKey(); + Certificate cert = keyStore.getCertificate("test"); + + XMLCipher keyCipher = XMLCipher.getInstance("http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p", + null, "http://www.w3.org/2001/04/xmlenc#sha512"); + keyCipher.init(XMLCipher.WRAP_MODE, cert.getPublicKey()); + EncryptedKey encryptedKey = keyCipher.encryptKey(doc, sessionKey); + + XMLCipher dataCipher = XMLCipher.getInstance("http://www.w3.org/2009/xmlenc11#aes256-gcm"); + dataCipher.init(XMLCipher.ENCRYPT_MODE, sessionKey); + + EncryptedData builder = dataCipher.getEncryptedData(); + KeyInfo builderKeyInfo = builder.getKeyInfo(); + if (builderKeyInfo == null) { + builderKeyInfo = new KeyInfo(doc); + builder.setKeyInfo(builderKeyInfo); + } + builderKeyInfo.add(encryptedKey); + + EncryptedData encData = dataCipher.encryptData(doc, null, new ByteArrayInputStream(data)); + Element encDataElement = dataCipher.martial(encData); + + doc.appendChild(encDataElement); + + return doc; + } +} diff --git a/src/test/java/org/apache/xml/security/test/dom/signature/SignatureFormattingTest.java b/src/test/java/org/apache/xml/security/test/dom/signature/SignatureFormattingTest.java new file mode 100644 index 000000000..11e485d7c --- /dev/null +++ b/src/test/java/org/apache/xml/security/test/dom/signature/SignatureFormattingTest.java @@ -0,0 +1,178 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.xml.security.test.dom.signature; + +import org.apache.xml.security.Init; +import org.apache.xml.security.formatting.FormattingChecker; +import org.apache.xml.security.formatting.FormattingCheckerFactory; +import org.apache.xml.security.formatting.FormattingTest; +import org.apache.xml.security.signature.XMLSignature; +import org.apache.xml.security.signature.XMLSignatureByteInput; +import org.apache.xml.security.signature.XMLSignatureInput; +import org.apache.xml.security.test.dom.DSNamespaceContext; +import org.apache.xml.security.test.dom.TestUtils; +import org.apache.xml.security.utils.Constants; +import org.apache.xml.security.utils.ElementProxy; +import org.apache.xml.security.utils.XMLUtils; +import org.apache.xml.security.utils.resolver.ResourceResolverContext; +import org.apache.xml.security.utils.resolver.ResourceResolverException; +import org.apache.xml.security.utils.resolver.ResourceResolverSpi; +import org.junit.jupiter.api.Test; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; + +import javax.xml.crypto.dsig.DigestMethod; +import javax.xml.xpath.XPath; +import javax.xml.xpath.XPathConstants; +import javax.xml.xpath.XPathExpressionException; +import javax.xml.xpath.XPathFactory; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.security.*; +import java.security.cert.X509Certificate; + +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +/** + * This is a {@link FormattingTest}, it is expected to be run with different system properties + * to check various formatting configurations. + * + * The test creates a detached signature with a single reference and uses mock resource resolver. + * RSA-2048 and SHA-512 are used to create longer binary values. + */ +@FormattingTest +public class SignatureFormattingTest { + private final static byte[] MOCK_DATA = new byte[]{ 0x0a, 0x0b, 0x0c, 0x0d }; + + private final FormattingChecker formattingChecker; + private KeyStore keyStore; + private XPath xpath; + private ResourceResolverSpi resolver; + + public SignatureFormattingTest() throws Exception { + Init.init(); + ElementProxy.setDefaultPrefix(Constants.SignatureSpecNS, "ds"); + formattingChecker = FormattingCheckerFactory.getFormattingChecker(); + keyStore = KeyStore.getInstance("PKCS12"); + try (InputStream in = getClass() + .getResourceAsStream("/org/apache/xml/security/samples/input/rsa.p12")) { + keyStore.load(in, "xmlsecurity".toCharArray()); + } catch (IOException | GeneralSecurityException e) { + fail("Cannot load test keystore", e); + } + + resolver = new TestResourceResolver(MOCK_DATA); + + XPathFactory xPathFactory = XPathFactory.newInstance(); + xpath = xPathFactory.newXPath(); + xpath.setNamespaceContext(new DSNamespaceContext()); + } + + @Test + public void testSignatureFormatting() throws Exception { + /* this test checks formatting of base64Binary values */ + Document doc = createDocument(); + + String docStr; + try (ByteArrayOutputStream out = new ByteArrayOutputStream()) { + XMLUtils.outputDOM(doc, out); + out.flush(); + docStr = out.toString(StandardCharsets.UTF_8); + } + + formattingChecker.checkDocument(docStr); + + XPathFactory xPathFactory = XPathFactory.newInstance(); + XPath xpath = xPathFactory.newXPath(); + xpath.setNamespaceContext(new DSNamespaceContext()); + + Element digest = findElementByXpath("//ds:DigestValue[1]", doc); + formattingChecker.checkBase64Value(digest.getTextContent()); + + Element signatureValue = findElementByXpath("//ds:SignatureValue[1]", doc); + formattingChecker.checkBase64ValueWithSpacing(signatureValue.getTextContent()); + + Element x509certValue = findElementByXpath("//ds:X509Certificate[1]", doc); + formattingChecker.checkBase64ValueWithSpacing(x509certValue.getTextContent()); + } + + @Test + public void testSignVerify() throws Exception { + /* this test checks the signature can be verified with given formatting settings */ + Document doc = createDocument(); + Element signatureElement = findElementByXpath("//ds:Signature[1]", doc); + XMLSignature signature = new XMLSignature(signatureElement, null); + signature.addResourceResolver(resolver); + + PublicKey publicKey = keyStore.getCertificate("test").getPublicKey(); + assertTrue(signature.checkSignatureValue(publicKey)); + } + + private Document createDocument() throws Exception { + Document doc = TestUtils.newDocument(); + + XMLSignature signature = new XMLSignature(doc, null, XMLSignature.ALGO_ID_SIGNATURE_RSA_SHA512); + signature.addResourceResolver(resolver); + + signature.addDocument("some.resource", null, DigestMethod.SHA512); + + PrivateKey privateKey = (PrivateKey) keyStore.getKey("test", "xmlsecurity".toCharArray()); + X509Certificate certificate = (X509Certificate) keyStore.getCertificate("test"); + + signature.addKeyInfo(certificate); + signature.sign(privateKey); + + doc.appendChild(signature.getElement()); + + return doc; + } + + private Element findElementByXpath(String expression, Node node) throws XPathExpressionException { + return (Element) xpath.evaluate(expression, node, XPathConstants.NODE); + } + + /** + * Resolver implementation which resolves every URI to the same given mock data. + */ + private static class TestResourceResolver extends ResourceResolverSpi { + private byte[] mockData; + + /** + * Creates new resolver. + * @param mockData Mock data bytes + */ + public TestResourceResolver(byte[] mockData) { + this.mockData = mockData; + } + + @Override + public XMLSignatureInput engineResolveURI(ResourceResolverContext context) throws ResourceResolverException { + return new XMLSignatureByteInput(mockData); + } + + @Override + public boolean engineCanResolveURI(ResourceResolverContext context) { + return true; + } + } +} diff --git a/src/test/java/org/apache/xml/security/test/stax/encryption/EncryptionFormattingTest.java b/src/test/java/org/apache/xml/security/test/stax/encryption/EncryptionFormattingTest.java new file mode 100644 index 000000000..fc0bf6ed5 --- /dev/null +++ b/src/test/java/org/apache/xml/security/test/stax/encryption/EncryptionFormattingTest.java @@ -0,0 +1,193 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.xml.security.test.stax.encryption; + +import org.apache.xml.security.Init; +import org.apache.xml.security.exceptions.XMLSecurityException; +import org.apache.xml.security.formatting.FormattingChecker; +import org.apache.xml.security.formatting.FormattingCheckerFactory; +import org.apache.xml.security.formatting.FormattingTest; +import org.apache.xml.security.stax.ext.*; +import org.apache.xml.security.stax.securityEvent.EncryptedElementSecurityEvent; +import org.apache.xml.security.stax.securityEvent.SecurityEvent; +import org.apache.xml.security.stax.securityEvent.SecurityEventConstants; +import org.apache.xml.security.stax.securityEvent.SecurityEventListener; +import org.apache.xml.security.stax.securityToken.SecurityTokenConstants; +import org.apache.xml.security.test.dom.DSNamespaceContext; +import org.apache.xml.security.test.stax.utils.XmlReaderToWriter; +import org.apache.xml.security.utils.XMLUtils; +import org.junit.jupiter.api.Test; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.NodeList; + +import javax.crypto.spec.SecretKeySpec; +import javax.xml.namespace.QName; +import javax.xml.stream.XMLInputFactory; +import javax.xml.stream.XMLStreamReader; +import javax.xml.stream.XMLStreamWriter; +import javax.xml.xpath.XPath; +import javax.xml.xpath.XPathConstants; +import javax.xml.xpath.XPathFactory; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.security.GeneralSecurityException; +import java.security.Key; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.cert.X509Certificate; +import java.util.List; +import java.util.Map; +import java.util.Random; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * This is a {@link FormattingTest}, it is expected to be run with different system properties + * to check various formatting configurations. + * + * The test encrypts part of the sample document using StAX API. + * Formatting of base64binary values is then checked. + * Also, decryption with StAX API is performed to ensure different formatting can be consumed. + */ +@FormattingTest +public class EncryptionFormattingTest { + private Random random = new Random(); + private final FormattingChecker formattingChecker; + private KeyStore keyStore; + private XPath xpath; + private XMLInputFactory xmlInputFactory; + + public EncryptionFormattingTest() throws KeyStoreException { + Init.init(); + formattingChecker = FormattingCheckerFactory.getFormattingChecker(); + keyStore = KeyStore.getInstance("PKCS12"); + try (InputStream in = getClass() + .getResourceAsStream("/org/apache/xml/security/samples/input/rsa.p12")) { + keyStore.load(in, "xmlsecurity".toCharArray()); + } catch (IOException | GeneralSecurityException e) { + fail("Cannot load test keystore", e); + } + + XPathFactory xPathFactory = XPathFactory.newInstance(); + xpath = xPathFactory.newXPath(); + xpath.setNamespaceContext(new DSNamespaceContext(Map.of( + "xenc", "http://www.w3.org/2001/04/xmlenc#" + ))); + + xmlInputFactory = XMLInputFactory.newInstance(); + } + + @Test + public void testEncryptedFormatting() throws Exception { + /* this test checks formatting of base64Binary values */ + byte[] documentBytes = createDocument(); + + /* + * The document retains a part of the original document, so we can't check the whole file linebreaks, + * i.e. formattingChecker.checkDocument(docStr); + */ + + /* parse as DOM to check base64 values */ + Document document; + try (InputStream in = new ByteArrayInputStream(documentBytes)) { + document = XMLUtils.read(in, false); + } + + /* + * In StAX implementation long element values are not surrounded by linebreaks, + * i.e. checkBase64ValueWithSpacing is not applicable. + */ + NodeList elements = (NodeList) xpath.evaluate("//xenc:CipherData", document, XPathConstants.NODESET); + assertEquals(2, elements.getLength()); + formattingChecker.checkBase64Value(elements.item(0).getTextContent()); + formattingChecker.checkBase64Value(elements.item(1).getTextContent()); + + Element x509certificate = + (Element) xpath.evaluate("//ds:X509Certificate", document, XPathConstants.NODE); + formattingChecker.checkBase64Value(x509certificate.getTextContent()); + } + + @Test + public void testEncryptDecrypt() throws Exception { + /* this test ensures that the encrypted data can be processed with various formatting settings */ + byte[] documentBytes = createDocument(); + + Key privateKey = keyStore.getKey("test", "xmlsecurity".toCharArray()); + + XMLSecurityProperties properties = new XMLSecurityProperties(); + properties.setDecryptionKey(privateKey); + InboundXMLSec inboundXMLSec = XMLSec.getInboundWSSec(properties); + + try (InputStream in = new ByteArrayInputStream(documentBytes)) { + XMLStreamReader reader = xmlInputFactory.createXMLStreamReader(in, StandardCharsets.UTF_8.name()); + DecryptionSecurityEventListener listener = new DecryptionSecurityEventListener(); + XMLStreamReader xmlSecReader = inboundXMLSec.processInMessage(reader, null, listener); + // read the document + while (xmlSecReader.hasNext()) xmlSecReader.next(); + xmlSecReader.close(); + } + } + + private Key generateSessionKey() { + byte[] keyBytes = new byte[32]; + random.nextBytes(keyBytes); + return new SecretKeySpec(keyBytes, "AES"); + } + + private byte[] createDocument() throws Exception { + X509Certificate certificate = (X509Certificate) keyStore.getCertificate("test"); + + Key sessionKey = generateSessionKey(); + + XMLSecurityProperties properties = new XMLSecurityProperties(); + properties.setActions(List.of(XMLSecurityConstants.ENCRYPTION)); + properties.setEncryptionKey(sessionKey); + properties.setEncryptionSymAlgorithm("http://www.w3.org/2009/xmlenc11#aes256-gcm"); + SecurePart securePart = + new SecurePart(new QName("urn:example:po", "PaymentInfo"), SecurePart.Modifier.Content); + properties.addEncryptionPart(securePart); + properties.setEncryptionTransportKey(certificate.getPublicKey()); + properties.setEncryptionUseThisCertificate(certificate); + properties.setEncryptionKeyIdentifier(SecurityTokenConstants.KeyIdentifier_X509KeyIdentifier); + + String plaintextResource = "/ie/baltimore/merlin-examples/merlin-xmlenc-five/plaintext.xml"; + try (InputStream in = getClass().getResourceAsStream(plaintextResource); + ByteArrayOutputStream out = new ByteArrayOutputStream()) { + XMLStreamReader reader = xmlInputFactory.createXMLStreamReader(in); + OutboundXMLSec outboundXMLSec = XMLSec.getOutboundXMLSec(properties); + XMLStreamWriter writer = outboundXMLSec.processOutMessage(out, StandardCharsets.UTF_8.name(), null); + XmlReaderToWriter.writeAllAndClose(reader, writer); + return out.toByteArray(); + } + } + + private static class DecryptionSecurityEventListener implements SecurityEventListener { + @Override + public void registerSecurityEvent(SecurityEvent securityEvent) throws XMLSecurityException { + if (SecurityEventConstants.EncryptedElement.equals(securityEvent.getSecurityEventType())) { + EncryptedElementSecurityEvent event = (EncryptedElementSecurityEvent) securityEvent; + assertTrue(event.isEncrypted()); + } + } + } +} diff --git a/src/test/java/org/apache/xml/security/test/stax/signature/SignatureFormattingTest.java b/src/test/java/org/apache/xml/security/test/stax/signature/SignatureFormattingTest.java new file mode 100644 index 000000000..2bb41ee55 --- /dev/null +++ b/src/test/java/org/apache/xml/security/test/stax/signature/SignatureFormattingTest.java @@ -0,0 +1,181 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.xml.security.test.stax.signature; + +import org.apache.xml.security.Init; +import org.apache.xml.security.exceptions.XMLSecurityException; +import org.apache.xml.security.formatting.FormattingChecker; +import org.apache.xml.security.formatting.FormattingCheckerFactory; +import org.apache.xml.security.formatting.FormattingTest; +import org.apache.xml.security.stax.ext.*; +import org.apache.xml.security.stax.securityEvent.*; +import org.apache.xml.security.stax.securityToken.SecurityTokenConstants; +import org.apache.xml.security.test.dom.DSNamespaceContext; +import org.apache.xml.security.test.stax.utils.XmlReaderToWriter; +import org.apache.xml.security.utils.Constants; +import org.apache.xml.security.utils.ElementProxy; +import org.apache.xml.security.utils.XMLUtils; +import org.junit.jupiter.api.Test; +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +import javax.xml.namespace.QName; +import javax.xml.stream.XMLInputFactory; +import javax.xml.stream.XMLStreamReader; +import javax.xml.stream.XMLStreamWriter; +import javax.xml.xpath.XPath; +import javax.xml.xpath.XPathConstants; +import javax.xml.xpath.XPathFactory; +import java.io.*; +import java.nio.charset.StandardCharsets; +import java.security.GeneralSecurityException; +import java.security.KeyStore; +import java.security.PrivateKey; +import java.security.cert.X509Certificate; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * This is a {@link FormattingTest}, it is expected to be run with different system properties + * to check various formatting configurations. + * + * The test adds an XML signature to a sample document using StAX API. + * Formatting of base64binary values is then checked. + * Also, signature verification with StAX API is performed to ensure different formatting can be consumed. + */ +@FormattingTest +public class SignatureFormattingTest { + private final FormattingChecker formattingChecker; + private KeyStore keyStore; + private XPath xpath; + private XMLInputFactory xmlInputFactory; + + public SignatureFormattingTest() throws Exception { + Init.init(); + ElementProxy.setDefaultPrefix(Constants.SignatureSpecNS, "ds"); + formattingChecker = FormattingCheckerFactory.getFormattingChecker(); + keyStore = KeyStore.getInstance("PKCS12"); + try (InputStream in = getClass() + .getResourceAsStream("/org/apache/xml/security/samples/input/rsa.p12")) { + keyStore.load(in, "xmlsecurity".toCharArray()); + } catch (IOException | GeneralSecurityException e) { + fail("Cannot load test keystore", e); + } + + XPathFactory xPathFactory = XPathFactory.newInstance(); + xpath = xPathFactory.newXPath(); + xpath.setNamespaceContext(new DSNamespaceContext()); + + xmlInputFactory = XMLInputFactory.newInstance(); + } + + @Test + public void testSignatureFormatting() throws Exception { + /* this test checks formatting of base64Binary values */ + byte[] documentBytes = createDocument(); + + /* + * The document retains a part of the original document, so we can't check the whole file linebreaks, + * i.e. formattingChecker.checkDocument(docStr); + */ + + /* parse as DOM to check base64 values */ + Document document; + try (InputStream in = new ByteArrayInputStream(documentBytes)) { + document = XMLUtils.read(in, false); + } + + /* + * In StAX implementation long element values are not surrounded by linebreaks, + * i.e. checkBase64ValueWithSpacing is not applicable. + */ + Element signatureValue = + (Element) xpath.evaluate("//ds:SignatureValue", document, XPathConstants.NODE); + formattingChecker.checkBase64Value(signatureValue.getTextContent()); + + Element x509certificate = + (Element) xpath.evaluate("//ds:X509Certificate", document, XPathConstants.NODE); + formattingChecker.checkBase64Value(x509certificate.getTextContent()); + } + + @Test + public void testSignVerify() throws Exception { + /* this test checks the signature can be verified with given formatting settings */ + byte[] documentBytes = createDocument(); + + XMLSecurityProperties properties = new XMLSecurityProperties(); + InboundXMLSec inboundXMLSec = XMLSec.getInboundWSSec(properties); + + try (InputStream in = new ByteArrayInputStream(documentBytes)) { + XMLStreamReader reader = xmlInputFactory.createXMLStreamReader(in, StandardCharsets.UTF_8.name()); + VerificationSecurityEventListener listener = new VerificationSecurityEventListener(); + XMLStreamReader xmlSecReader = inboundXMLSec.processInMessage(reader, null, listener); + // read the document + while (xmlSecReader.hasNext()) xmlSecReader.next(); + xmlSecReader.close(); + assertTrue(listener.isSignatureVerified()); + } + } + + private byte[] createDocument() throws Exception { + PrivateKey privateKey = (PrivateKey) keyStore.getKey("test", "xmlsecurity".toCharArray()); + X509Certificate certificate = (X509Certificate) keyStore.getCertificate("test"); + + XMLSecurityProperties properties = new XMLSecurityProperties(); + properties.setActions(List.of(XMLSecurityConstants.SIGNATURE)); + properties.setSignatureKey(privateKey); + properties.setSignatureCerts(new X509Certificate[]{ certificate }); + SecurePart securePart = + new SecurePart(new QName("urn:example:po", "PaymentInfo"), SecurePart.Modifier.Content); + properties.addSignaturePart(securePart); + properties.setSignatureAlgorithm("http://www.w3.org/2001/04/xmldsig-more#rsa-sha512"); + properties.setSignatureKeyIdentifier(SecurityTokenConstants.KeyIdentifier_X509KeyIdentifier); + + String plaintextResource = "/ie/baltimore/merlin-examples/merlin-xmlenc-five/plaintext.xml"; + try (InputStream in = getClass().getResourceAsStream(plaintextResource); + ByteArrayOutputStream out = new ByteArrayOutputStream()) { + XMLStreamReader reader = xmlInputFactory.createXMLStreamReader(in); + OutboundXMLSec outboundXMLSec = XMLSec.getOutboundXMLSec(properties); + XMLStreamWriter writer = outboundXMLSec.processOutMessage(out, StandardCharsets.UTF_8.name(), null); + XmlReaderToWriter.writeAllAndClose(reader, writer); + return out.toByteArray(); + } + } + + private static class VerificationSecurityEventListener implements SecurityEventListener { + boolean signatureVerified = false; + + public boolean isSignatureVerified() { + return signatureVerified; + } + + @Override + public void registerSecurityEvent(SecurityEvent securityEvent) throws XMLSecurityException { + if (SecurityEventConstants.SignatureValue.equals(securityEvent.getSecurityEventType())) { + SignatureValueSecurityEvent event = (SignatureValueSecurityEvent) securityEvent; + assertNotNull(event.getSignatureValue()); + signatureVerified = true; + } else if (SecurityEventConstants.SignedElement.equals(securityEvent.getSecurityEventType())) { + SignedElementSecurityEvent event = (SignedElementSecurityEvent) securityEvent; + assertTrue(event.isSigned()); + } + } + } +} diff --git a/src/test/resources/org/apache/xml/security/samples/input/rsa.p12 b/src/test/resources/org/apache/xml/security/samples/input/rsa.p12 new file mode 100644 index 0000000000000000000000000000000000000000..d1d4011c5de64039013d418bdee96abd9eecdf06 GIT binary patch literal 2612 zcma);RaDfA7RH&x0mdN)C8Rt3k2KOXh_uvzC^ZTaf;1?|(9%dsm!QC*6cLb?9vF}m zr5h!rMc{bXz00%i)4dOS?X|yefBW&bHb9^Nz#s@fKzxQH1j97L4v`>oP%Z({2PGi7 ze#`9u0rvX8ELaAV089NXKmT2EaLWIrs3<_-Tmnq#w6b~0h3$U9@oYvL#AR|aVXP2joGN*W7H}Ffm zYzI?yz+Sif#gb<*spnLruXeYRy>xZ^QR(nNQuiB~@)zKI{|`0Y1y5q0LSqfORr%M( zESmy3-p+7$EYBrqADtRDI|?ILjy;(@-7k4<+;bnvka}hjlgczy5H-PUeW-#Q)hbQ8 z5H-@-_TG&cDQsc1eq1P5@@afIBPZ|4p>Q|=6(4Qf3ljOUTQTue|dsMBL7|iJln<$*E-j{Wyy^3xn zeg;*gTvrBmjM>P?4M7FvUnH3+ZmoaV=Di-D^-?VFXSfm!p{OJoqVh}5&%S&lLmj+t zD8?0bZylw8g<;_aJ^-HP(bUqZ(x$q4LsSFP!HkL)i%!z%Luc0F#d(TY^;=VwT3L0o zkB$Vo?H4td9#_sB>9Jm_YELZ;2;g_Ahj^mn=xIV;pdIfK!jN!V{@m9nI;Q~U51=ta zy8PJW&5xZWIe=N|3Nnk+!HzSFC+_kwu3kk5vRUAJ<>OB=v*W<1+>Y}b-x*XbB<#m67JCdfX zC77G0l=?NP)dthzw|qz$PV;P%hZ-@A{gGVlRBH|sW4tI;e08Nssa>-+tmc8#>B*r?<|lJl zIi1;g<2kBnTu4cc-!hT3HU2C2W%XX=>yk;vPT=uI0_gFC)YrP_ZuiNLVWm`$S=PC) zv1`pO8Q4+26XC~x?qcg6Q(&goAU*Pe)F9hd`ukGjBEl$f3SmngSw{gE(wzHlbV&cy zH^Uj6uYEhKr>62KRFWMEK6dDsHSv}k&B_bodDAEDPw9*d5}=#$ej&MO%{G#6t(F|H zpCqy-$mhD>N1ORZz5riF?kdLm(=}H##WS+35N^@_61kq>&NUq~zkZ-<(#rfq0+DIT zGO0~?9Lv9*{+%_q?0Y2k#j|~1pYmd#3Y|mt3pX-_3{$!Ce(Lgh%Dg<_EcBnS3>%V>>?ya&rYif=n&ZtIv9O09`d)E`b8}> zF>=7=Ke$AoF%+y9$UMmKWbR}h0LMQDJq8V>HF9vqvty(r#U-UA0SPe)X>ou6rTMc& z4$mb(!2khr1_qJ+ZioLkp#OzvtrFV!tz@0lY#GxeY!;o_O|2#S{|C_uWw%$+3YSx4 z@V1xU7KJiDB|x6O_uqbR7OK{QM&r%Oa#;+Gjk&XtbaEby!Yt}m-yTm2ms2}4k|0m~ zX^0Q8NGhE1My-T)Dv{N;TBH52)c5k%LYu`A30&_N(Q*(G-|0NoSakBr-u`NE+T5Fy z07dh@QGIP&xr(V(6EOz&Xl~u(INENtido-mvcIkrdjDr-QaacY?p(d z(=BdsUZwE|9BcE8Vxcqo`sm`01Ygnu4Ew_BD&URt6L{MCDTKk*w~`WHz|9WbRB-s2K=Q8 zVWt5nn$=ik=c5K4n0lSOU1Gh%%ce$aN*t%VMVT&MjAD62?)R0No<@kRxiU~c>HY^g z^}<78*-!XU|BQC${Gjc52SKqrRY7<^N$%NXjZ&rIB0f~X#pqtPEs2v$zRLT~iN|WP zU#yru3J#B{hVcXq^~bpy1WzvjY)jb|=4lS`UTV@Ng^j}wSm^@@u<%1dNr(6FCuN$k1B>typ^xgK~0N2c+Gs~!9PRc~7C zR!$LW_5cfNqPfB<6tQBwv#C#W#v{~=tQT|46fbi3O&RyHU8ZmFI9?aZP+l=?h)v&9 zzLtDy^b$N4u*V?4P7*Nb(=6n4id3@(JL^Tn^e5$TgtfKbRjeO^m&3Yb_1x*c8bJFZ zua-*qaNea3L4j_hczDHss)sv0Ha*)6upu72eKuhYqI)ZOq9EHDD5HI?$pTvkmOveb z+Dj{y%r1tAC{iY>IUVE)^d#Pp-xAJ|X}lHDO}OZSc68)^i0k+$G5P+IpF*F&ZD~A2 zsbWplG+j-je>>FP<$Jz^P_OHTN%b8hMV|GRV1Ytxda-CR1#!RVi=JH_8ra>d`nYSw z#!#m3{WCfP8Wh7-Z3J0hV#(C?VWKUyaSTH!X5V5O^-=Qll&HgGE(ex`|E zdtL}o#L8%2Tw=ixzvu9sSACdPtpIcY41oOeNr6EyGAKKUKc%FqX4ffHKtCpagg%l$ s#ozRRBfX-BRJ?R?_6iz`HcKR4Wsk6f`1FL#{xzBYL^JlQ*S`?(H>?Svs{jB1 literal 0 HcmV?d00001 From 969eb8d394f60ed9c5f1d28c79487bfef43301ca Mon Sep 17 00:00:00 2001 From: Ihor Kuzmanenko Date: Sat, 6 Dec 2025 16:45:12 +0200 Subject: [PATCH 6/6] fixed grammar and codestyle in XMLUtilsTest.java --- .../org/apache/xml/security/utils/XMLUtilsTest.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/test/java/org/apache/xml/security/utils/XMLUtilsTest.java b/src/test/java/org/apache/xml/security/utils/XMLUtilsTest.java index 18cc1119b..0556b8fd5 100644 --- a/src/test/java/org/apache/xml/security/utils/XMLUtilsTest.java +++ b/src/test/java/org/apache/xml/security/utils/XMLUtilsTest.java @@ -43,7 +43,7 @@ * Output of the first two methods is checked using an appropriate {@link FormattingChecker} implementation. * The result of stream encoding is compared to the output of {@code encodeToString} method. * - * There are also tests, which check that the corresponding decoding methods can process Base64-encoded data with any + * There are also tests which check that the corresponding decoding methods can process Base64-encoded data with any * formatting regardless of formatting options. */ @FormattingTest @@ -56,7 +56,7 @@ public class XMLUtilsTest { @Test public void testEncodeToString() { - byte[] data = new byte[60]; // long enough for a line break in MIME encoding + byte[] data = new byte[60]; // long enough for a line break in MIME encoding String encoded = XMLUtils.encodeToString(data); formattingChecker.checkBase64Value(encoded); } @@ -70,21 +70,21 @@ public void testEncodeToStringShort() { @Test public void testEncodeElementValue() { - byte[] data = new byte[60]; // long enough for a line break in MIME encoding + byte[] data = new byte[60]; // long enough for a line break in MIME encoding String encoded = XMLUtils.encodeElementValue(data); formattingChecker.checkBase64ValueWithSpacing(encoded); } @Test public void testEncodeElementValueShort() { - byte[] data = new byte[8]; + byte[] data = new byte[8]; String encoded = XMLUtils.encodeElementValue(data); formattingChecker.checkBase64ValueWithSpacing(encoded); } @Test public void testEncodeUsingStream() throws IOException { - byte[] data = new byte[60]; + byte[] data = new byte[60]; String expected = XMLUtils.encodeToString(data); String encodedWithStream; try (ByteArrayOutputStream encoded = new ByteArrayOutputStream();