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/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..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
@@ -40,7 +40,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 +174,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..a375044e5 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,38 @@
/**
* DOM and XML accessibility and comfort functions.
*
+ * @implNote
+ * 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
+ * - {@systemProperty org.apache.xml.security.base64.ignoreLineBreaks} - ignores line breaks in base64Binary values.
+ * Takes precedence over line length and separator options (see below). Default: false
+ * - {@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 the nearest multiple of 4. Values less than 4 are ignored. Default: 76
+ *
*/
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 boolean ignoreLineBreaks =
AccessController.doPrivileged(
- (PrivilegedAction) () -> Boolean.getBoolean("org.apache.xml.security.ignoreLineBreaks"));
+ (PrivilegedAction) () -> Boolean.getBoolean(IGNORE_LINE_BREAKS_PROP));
- private static final Logger LOG = System.getLogger(XMLUtils.class.getName());
+ private static Base64FormattingOptions base64Formatting =
+ AccessController.doPrivileged(
+ (PrivilegedAction) () -> new Base64FormattingOptions());
+
+ 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 +539,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 +1122,90 @@ public static byte[] getBytes(BigInteger big, int bitlen) {
return resizedBytes;
}
+
+ /**
+ * 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;
+
+ /**
+ * Creates new formatting options by reading system properties.
+ */
+ 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 boolean isIgnoreLineBreaks() {
+ return ignoreLineBreaks;
+ }
+
+ public Base64LineSeparator getLineSeparator() {
+ return lineSeparator;
+ }
+
+ public int getLineLength() {
+ return lineLength;
+ }
+ }
+
+ enum Base64LineSeparator {
+ CRLF(new byte[]{'\r', '\n'}),
+ LF(new byte[]{'\n'});
+
+ private byte[] bytes;
+
+ Base64LineSeparator(byte[] bytes) {
+ this.bytes = bytes;
+ }
+
+ byte[] getBytes() {
+ return bytes;
+ }
+ }
}
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/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/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..0556b8fd5
--- /dev/null
+++ b/src/test/java/org/apache/xml/security/utils/XMLUtilsTest.java
@@ -0,0 +1,141 @@
+/**
+ * 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.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.*;
+import java.nio.charset.StandardCharsets;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * This test checks {@link XMLUtils} class methods, responsible for Base64 values formatting in XML documents.
+ * 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}:
+ *
+ * - {@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[])})
+ *
+ * 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 FormattingChecker formattingChecker = FormattingCheckerFactory.getFormattingChecker();
+
+ /* 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 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 testEncodeToStringShort() {
+ byte[] data = new byte[8];
+ String encoded = XMLUtils.encodeToString(data);
+ formattingChecker.checkBase64Value(encoded);
+ }
+
+ @Test
+ 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 testEncodeElementValueShort() {
+ byte[] data = new byte[8];
+ String encoded = XMLUtils.encodeElementValue(data);
+ formattingChecker.checkBase64ValueWithSpacing(encoded);
+ }
+
+ @Test
+ 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(expected, encodedWithStream);
+ }
+
+ @Test
+ public void decodeNoLineBreaks() {
+ String encoded = "AQIDBAUGBwg=";
+
+ byte[] data = XMLUtils.decode(encoded);
+ assertArrayEquals(TEST_DATA, data);
+
+ data = XMLUtils.decode(encoded.getBytes(StandardCharsets.US_ASCII));
+ assertArrayEquals(TEST_DATA, data);
+ }
+
+ @Test
+ public void decodeCrlfLineBreaks() {
+ String encoded = "AQIDBAUG\r\nBwg=";
+
+ byte[] data = XMLUtils.decode(encoded);
+ assertArrayEquals(TEST_DATA, data);
+
+ data = XMLUtils.decode(encoded.getBytes(StandardCharsets.US_ASCII));
+ assertArrayEquals(TEST_DATA, data);
+ }
+
+ @Test
+ public void decodeLfLineBreaks() {
+ String encoded = "AQIDBAUG\nBwg=";
+
+ byte[] data = XMLUtils.decode(encoded);
+ assertArrayEquals(TEST_DATA, data);
+
+ data = XMLUtils.decode(encoded.getBytes(StandardCharsets.US_ASCII));
+ assertArrayEquals(TEST_DATA, data);
+ }
+
+ @Test
+ public void decodeStream() throws IOException {
+ byte[] encodedBytes = "AQIDBAUGBwg=".getBytes(StandardCharsets.US_ASCII);
+
+ 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
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 000000000..d1d4011c5
Binary files /dev/null and b/src/test/resources/org/apache/xml/security/samples/input/rsa.p12 differ