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: + * */ 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